retold-remote 0.0.23 → 0.0.26
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/css/retold-remote.css +343 -20
- package/docs/.nojekyll +0 -0
- package/docs/README.md +64 -12
- package/docs/_cover.md +6 -6
- package/docs/_sidebar.md +2 -0
- package/docs/_topbar.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/collections.md +30 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +62 -2
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +254 -0
- package/docs/retold-keyword-index.json +31216 -0
- package/docs/server-setup.md +122 -91
- package/docs/stack-launcher.md +218 -0
- package/docs/synology.md +585 -0
- package/docs/ultravisor-configuration.md +5 -5
- package/docs/ultravisor-integration.md +4 -2
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +22 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +460 -7
- package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
- package/source/cli/RetoldRemote-Stack-Run.js +41 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
- package/source/providers/CollectionManager-AddItems.js +166 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
- package/source/providers/Pict-Provider-OperationStatus.js +597 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
- package/source/server/RetoldRemote-CollectionExportService.js +763 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +218 -3
- package/source/server/RetoldRemote-ImageService.js +221 -46
- package/source/server/RetoldRemote-MediaService.js +63 -4
- package/source/server/RetoldRemote-MetadataCache.js +25 -5
- package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
- package/source/server/RetoldRemote-SubimageService.js +680 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
- package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
- package/source/server/RetoldRemote-VideoFrameService.js +302 -9
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +1050 -0
- package/source/views/PictView-Remote-AudioExplorer.js +77 -1
- package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
- package/source/views/PictView-Remote-Gallery.js +365 -64
- package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +58 -0
- package/source/views/PictView-Remote-MediaViewer.js +100 -25
- package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/source/views/PictView-Remote-TopBar.js +1 -0
- package/source/views/PictView-Remote-VideoExplorer.js +77 -1
- package/web-application/css/docuserve.css +277 -23
- package/web-application/css/retold-remote.css +343 -20
- package/web-application/docs/README.md +64 -12
- package/web-application/docs/_cover.md +6 -6
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/_topbar.md +1 -1
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +62 -2
- package/web-application/docs/server-setup.md +122 -91
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/synology.md +585 -0
- package/web-application/docs/ultravisor-configuration.md +5 -5
- package/web-application/docs/ultravisor-integration.md +4 -2
- package/web-application/js/pict-docuserve.min.js +12 -12
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +6596 -1784
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +75 -23
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -15,6 +15,14 @@ const _ViewConfiguration =
|
|
|
15
15
|
Renderables: []
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// Chunked rendering tuning constants
|
|
19
|
+
const _CHUNKED_RENDER_THRESHOLD = 500; // Below this, render synchronously in one shot
|
|
20
|
+
const _CHUNK_FIRST_SIZE = 250; // First chunk — appears within one frame
|
|
21
|
+
const _CHUNK_SUBSEQUENT_SIZE = 500; // Later chunks — larger for throughput
|
|
22
|
+
|
|
23
|
+
// Thumbnail concurrency cap
|
|
24
|
+
const _MAX_THUMBNAIL_CONCURRENCY = 8;
|
|
25
|
+
|
|
18
26
|
class RetoldRemoteGalleryView extends libPictView
|
|
19
27
|
{
|
|
20
28
|
constructor(pFable, pOptions, pServiceHash)
|
|
@@ -22,6 +30,59 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
22
30
|
super(pFable, pOptions, pServiceHash);
|
|
23
31
|
|
|
24
32
|
this._intersectionObserver = null;
|
|
33
|
+
|
|
34
|
+
// Chunked render state
|
|
35
|
+
this._activeRenderFrame = null; // requestAnimationFrame id
|
|
36
|
+
this._activeRenderToken = 0; // incremented to invalidate in-flight chunked renders
|
|
37
|
+
|
|
38
|
+
// Thumbnail loading queue
|
|
39
|
+
this._thumbnailQueue = [];
|
|
40
|
+
this._thumbnailInFlight = 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Paint an immediate loading overlay in the gallery container.
|
|
45
|
+
* Called by the application's loadFileList() before the fetch starts
|
|
46
|
+
* so the user sees instant feedback when they click a folder.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} pPath - Path being loaded (for display)
|
|
49
|
+
*/
|
|
50
|
+
showLoadingState(pPath)
|
|
51
|
+
{
|
|
52
|
+
let tmpContainer = document.getElementById('RetoldRemote-Gallery-Container');
|
|
53
|
+
if (!tmpContainer)
|
|
54
|
+
{
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let tmpFmt = this.pict.providers['RetoldRemote-FormattingUtilities'];
|
|
59
|
+
let tmpEscaped = tmpFmt ? tmpFmt.escapeHTML(pPath || '') : (pPath || '');
|
|
60
|
+
|
|
61
|
+
let tmpHTML = '<div class="retold-remote-gallery-loading">';
|
|
62
|
+
tmpHTML += '<div class="retold-remote-gallery-loading-spinner"></div>';
|
|
63
|
+
tmpHTML += '<div class="retold-remote-gallery-loading-text">Loading folder\u2026</div>';
|
|
64
|
+
if (tmpEscaped)
|
|
65
|
+
{
|
|
66
|
+
tmpHTML += '<div class="retold-remote-gallery-loading-path">' + tmpEscaped + '</div>';
|
|
67
|
+
}
|
|
68
|
+
tmpHTML += '</div>';
|
|
69
|
+
|
|
70
|
+
tmpContainer.innerHTML = tmpHTML;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Cancel any chunked render currently in flight.
|
|
75
|
+
* Call this before starting a new render, or when navigating away.
|
|
76
|
+
*/
|
|
77
|
+
cancelActiveRender()
|
|
78
|
+
{
|
|
79
|
+
if (this._activeRenderFrame !== null)
|
|
80
|
+
{
|
|
81
|
+
cancelAnimationFrame(this._activeRenderFrame);
|
|
82
|
+
this._activeRenderFrame = null;
|
|
83
|
+
}
|
|
84
|
+
// Bump the token so any in-flight chunked render from a closure bails out
|
|
85
|
+
this._activeRenderToken++;
|
|
25
86
|
}
|
|
26
87
|
|
|
27
88
|
// ──────────────────────────────────────────────
|
|
@@ -31,6 +92,10 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
31
92
|
/**
|
|
32
93
|
* Render the gallery based on current state.
|
|
33
94
|
* GalleryItems is already filtered+sorted by the pipeline provider.
|
|
95
|
+
*
|
|
96
|
+
* For folders with more than _CHUNKED_RENDER_THRESHOLD items, the render
|
|
97
|
+
* is split into chunks scheduled via requestAnimationFrame so the UI
|
|
98
|
+
* stays responsive. Smaller folders use the synchronous fast path.
|
|
34
99
|
*/
|
|
35
100
|
renderGallery()
|
|
36
101
|
{
|
|
@@ -40,6 +105,10 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
40
105
|
return;
|
|
41
106
|
}
|
|
42
107
|
|
|
108
|
+
// Cancel any chunked render already in flight — fast folder-to-folder
|
|
109
|
+
// navigation should not stack up render work.
|
|
110
|
+
this.cancelActiveRender();
|
|
111
|
+
|
|
43
112
|
let tmpRemote = this.pict.AppData.RetoldRemote;
|
|
44
113
|
let tmpItems = tmpRemote.GalleryItems || [];
|
|
45
114
|
let tmpViewMode = tmpRemote.ViewMode || 'list';
|
|
@@ -69,61 +138,191 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
69
138
|
tmpHTML += '<div>Empty folder</div>';
|
|
70
139
|
tmpHTML += '</div>';
|
|
71
140
|
tmpContainer.innerHTML = tmpHTML;
|
|
141
|
+
this._restoreSearchFocus(tmpSearchHadFocus, tmpSearchSelStart, tmpSearchSelEnd);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
72
144
|
|
|
73
|
-
|
|
74
|
-
|
|
145
|
+
// SMALL FOLDER FAST PATH: for ≤ _CHUNKED_RENDER_THRESHOLD items, render
|
|
146
|
+
// everything synchronously in a single innerHTML assignment. This
|
|
147
|
+
// preserves the existing behavior for normal-sized folders.
|
|
148
|
+
if (tmpItems.length <= _CHUNKED_RENDER_THRESHOLD)
|
|
149
|
+
{
|
|
150
|
+
if (tmpViewMode === 'gallery')
|
|
75
151
|
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
152
|
+
tmpHTML += this._buildGridHTML(tmpItems, tmpThumbnailSize, tmpCursorIndex);
|
|
153
|
+
}
|
|
154
|
+
else
|
|
155
|
+
{
|
|
156
|
+
tmpHTML += this._buildListHTML(tmpItems, tmpCursorIndex);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
tmpContainer.innerHTML = tmpHTML;
|
|
160
|
+
this._restoreSearchFocus(tmpSearchHadFocus, tmpSearchSelStart, tmpSearchSelEnd);
|
|
161
|
+
this._setupLazyLoading();
|
|
162
|
+
|
|
163
|
+
let tmpNavProvider = this.pict.providers['RetoldRemote-GalleryNavigation'];
|
|
164
|
+
if (tmpNavProvider)
|
|
165
|
+
{
|
|
166
|
+
tmpNavProvider.recalculateColumns();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let tmpTopBarView = this.pict.views['ContentEditor-TopBar'];
|
|
170
|
+
if (tmpTopBarView && tmpTopBarView.updateFilterIcon)
|
|
171
|
+
{
|
|
172
|
+
tmpTopBarView.updateFilterIcon();
|
|
82
173
|
}
|
|
83
174
|
return;
|
|
84
175
|
}
|
|
85
176
|
|
|
86
|
-
//
|
|
177
|
+
// LARGE FOLDER CHUNKED PATH: paint the scaffolding now, then fill the
|
|
178
|
+
// items container in chunks via requestAnimationFrame so the main
|
|
179
|
+
// thread stays responsive.
|
|
180
|
+
let tmpItemsContainerID = 'RetoldRemote-GalleryItemsContainer';
|
|
181
|
+
let tmpProgressID = 'RetoldRemote-GalleryProgress';
|
|
182
|
+
|
|
87
183
|
if (tmpViewMode === 'gallery')
|
|
88
184
|
{
|
|
89
|
-
tmpHTML +=
|
|
185
|
+
tmpHTML += '<div class="retold-remote-grid size-' + tmpThumbnailSize + '" id="' + tmpItemsContainerID + '"></div>';
|
|
90
186
|
}
|
|
91
187
|
else
|
|
92
188
|
{
|
|
93
|
-
tmpHTML +=
|
|
189
|
+
tmpHTML += '<div class="retold-remote-list" id="' + tmpItemsContainerID + '"></div>';
|
|
94
190
|
}
|
|
95
191
|
|
|
192
|
+
tmpHTML += '<div class="retold-remote-gallery-progress" id="' + tmpProgressID + '">'
|
|
193
|
+
+ '<span class="retold-remote-gallery-progress-spinner"></span>'
|
|
194
|
+
+ '<span class="retold-remote-gallery-progress-text">Rendering 0 / ' + tmpItems.length.toLocaleString() + '\u2026</span>'
|
|
195
|
+
+ '</div>';
|
|
196
|
+
|
|
96
197
|
tmpContainer.innerHTML = tmpHTML;
|
|
198
|
+
this._restoreSearchFocus(tmpSearchHadFocus, tmpSearchSelStart, tmpSearchSelEnd);
|
|
97
199
|
|
|
98
|
-
//
|
|
99
|
-
|
|
200
|
+
// Update the top bar filter icon state right away (doesn't need items)
|
|
201
|
+
let tmpTopBarView = this.pict.views['ContentEditor-TopBar'];
|
|
202
|
+
if (tmpTopBarView && tmpTopBarView.updateFilterIcon)
|
|
100
203
|
{
|
|
101
|
-
|
|
102
|
-
if (tmpNewSearch)
|
|
103
|
-
{
|
|
104
|
-
tmpNewSearch.focus();
|
|
105
|
-
tmpNewSearch.setSelectionRange(tmpSearchSelStart, tmpSearchSelEnd);
|
|
106
|
-
}
|
|
204
|
+
tmpTopBarView.updateFilterIcon();
|
|
107
205
|
}
|
|
108
206
|
|
|
109
|
-
//
|
|
110
|
-
this.
|
|
207
|
+
// Start the chunked fill
|
|
208
|
+
this._renderGalleryChunked(tmpItems, tmpViewMode, tmpThumbnailSize, tmpCursorIndex,
|
|
209
|
+
tmpItemsContainerID, tmpProgressID);
|
|
210
|
+
}
|
|
111
211
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Restore search input focus and selection range after a full re-render.
|
|
214
|
+
*/
|
|
215
|
+
_restoreSearchFocus(pHadFocus, pSelStart, pSelEnd)
|
|
216
|
+
{
|
|
217
|
+
if (!pHadFocus)
|
|
115
218
|
{
|
|
116
|
-
|
|
219
|
+
return;
|
|
117
220
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
let tmpTopBarView = this.pict.views['ContentEditor-TopBar'];
|
|
121
|
-
if (tmpTopBarView && tmpTopBarView.updateFilterIcon)
|
|
221
|
+
let tmpNewSearch = document.getElementById('RetoldRemote-Gallery-Search');
|
|
222
|
+
if (tmpNewSearch)
|
|
122
223
|
{
|
|
123
|
-
|
|
224
|
+
tmpNewSearch.focus();
|
|
225
|
+
tmpNewSearch.setSelectionRange(pSelStart, pSelEnd);
|
|
124
226
|
}
|
|
125
227
|
}
|
|
126
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Render the gallery items into the scaffolding container in chunks,
|
|
231
|
+
* scheduling each chunk with requestAnimationFrame so the main thread
|
|
232
|
+
* stays responsive. Updates the progress strip between chunks.
|
|
233
|
+
*
|
|
234
|
+
* @param {Array} pItems - Items to render
|
|
235
|
+
* @param {string} pViewMode - 'gallery' or 'list'
|
|
236
|
+
* @param {string} pThumbnailSize - 'small' | 'medium' | 'large'
|
|
237
|
+
* @param {number} pCursorIndex - Currently selected index
|
|
238
|
+
* @param {string} pItemsContainerID - DOM id of the inner items container
|
|
239
|
+
* @param {string} pProgressID - DOM id of the progress strip
|
|
240
|
+
*/
|
|
241
|
+
_renderGalleryChunked(pItems, pViewMode, pThumbnailSize, pCursorIndex, pItemsContainerID, pProgressID)
|
|
242
|
+
{
|
|
243
|
+
let tmpSelf = this;
|
|
244
|
+
let tmpToken = ++this._activeRenderToken;
|
|
245
|
+
let tmpTotal = pItems.length;
|
|
246
|
+
let tmpOffset = 0;
|
|
247
|
+
|
|
248
|
+
let _renderNextChunk = function ()
|
|
249
|
+
{
|
|
250
|
+
// If another render started while we were waiting, bail out
|
|
251
|
+
if (tmpToken !== tmpSelf._activeRenderToken)
|
|
252
|
+
{
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let tmpItemsContainer = document.getElementById(pItemsContainerID);
|
|
257
|
+
if (!tmpItemsContainer)
|
|
258
|
+
{
|
|
259
|
+
// Container was replaced (e.g., navigated away) — stop rendering
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// First chunk is smaller so it paints within one frame; later chunks
|
|
264
|
+
// are larger for throughput
|
|
265
|
+
let tmpChunkSize = (tmpOffset === 0) ? _CHUNK_FIRST_SIZE : _CHUNK_SUBSEQUENT_SIZE;
|
|
266
|
+
let tmpEnd = Math.min(tmpOffset + tmpChunkSize, tmpTotal);
|
|
267
|
+
let tmpSlice = pItems.slice(tmpOffset, tmpEnd);
|
|
268
|
+
|
|
269
|
+
let tmpChunkHTML;
|
|
270
|
+
if (pViewMode === 'gallery')
|
|
271
|
+
{
|
|
272
|
+
tmpChunkHTML = tmpSelf._buildGridItemsHTML(tmpSlice, pCursorIndex, tmpOffset);
|
|
273
|
+
}
|
|
274
|
+
else
|
|
275
|
+
{
|
|
276
|
+
tmpChunkHTML = tmpSelf._buildListItemsHTML(tmpSlice, pCursorIndex, tmpOffset);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
tmpItemsContainer.insertAdjacentHTML('beforeend', tmpChunkHTML);
|
|
280
|
+
|
|
281
|
+
tmpOffset = tmpEnd;
|
|
282
|
+
|
|
283
|
+
// Update progress strip
|
|
284
|
+
let tmpProgressEl = document.getElementById(pProgressID);
|
|
285
|
+
if (tmpProgressEl)
|
|
286
|
+
{
|
|
287
|
+
let tmpProgressText = tmpProgressEl.querySelector('.retold-remote-gallery-progress-text');
|
|
288
|
+
if (tmpProgressText)
|
|
289
|
+
{
|
|
290
|
+
tmpProgressText.textContent = 'Rendering ' + tmpOffset.toLocaleString() + ' / ' + tmpTotal.toLocaleString() + '\u2026';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (tmpOffset < tmpTotal)
|
|
295
|
+
{
|
|
296
|
+
// Incrementally wire up lazy loading for the chunk just appended
|
|
297
|
+
// so thumbnails start loading while later chunks are still rendering
|
|
298
|
+
tmpSelf._observeNewThumbnails();
|
|
299
|
+
tmpSelf._activeRenderFrame = requestAnimationFrame(_renderNextChunk);
|
|
300
|
+
}
|
|
301
|
+
else
|
|
302
|
+
{
|
|
303
|
+
// Final chunk rendered — tear down progress strip and finalize
|
|
304
|
+
tmpSelf._activeRenderFrame = null;
|
|
305
|
+
|
|
306
|
+
if (tmpProgressEl && tmpProgressEl.parentElement)
|
|
307
|
+
{
|
|
308
|
+
tmpProgressEl.parentElement.removeChild(tmpProgressEl);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
tmpSelf._setupLazyLoading();
|
|
312
|
+
|
|
313
|
+
let tmpNavProvider = tmpSelf.pict.providers['RetoldRemote-GalleryNavigation'];
|
|
314
|
+
if (tmpNavProvider)
|
|
315
|
+
{
|
|
316
|
+
tmpNavProvider.recalculateColumns();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Kick off the first chunk on the next frame — gives the browser time
|
|
322
|
+
// to paint the scaffolding + progress strip first
|
|
323
|
+
this._activeRenderFrame = requestAnimationFrame(_renderNextChunk);
|
|
324
|
+
}
|
|
325
|
+
|
|
127
326
|
// ──────────────────────────────────────────────
|
|
128
327
|
// Header
|
|
129
328
|
// ──────────────────────────────────────────────
|
|
@@ -413,25 +612,44 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
413
612
|
// ──────────────────────────────────────────────
|
|
414
613
|
|
|
415
614
|
/**
|
|
416
|
-
* Build the grid view HTML.
|
|
615
|
+
* Build the grid view HTML — full wrapper + all items.
|
|
616
|
+
* Used by the synchronous fast path for small folders.
|
|
417
617
|
*/
|
|
418
618
|
_buildGridHTML(pItems, pThumbnailSize, pCursorIndex)
|
|
419
619
|
{
|
|
420
620
|
let tmpHTML = '<div class="retold-remote-grid size-' + pThumbnailSize + '">';
|
|
621
|
+
tmpHTML += this._buildGridItemsHTML(pItems, pCursorIndex, 0);
|
|
622
|
+
tmpHTML += '</div>';
|
|
623
|
+
return tmpHTML;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Build the per-item HTML for a grid view slice.
|
|
628
|
+
*
|
|
629
|
+
* @param {Array} pItems - The items to render (may be a slice of the full list)
|
|
630
|
+
* @param {number} pCursorIndex - Index of the currently-selected item in the FULL list
|
|
631
|
+
* @param {number} pStartIndex - Index of pItems[0] within the full list (for data-index and click handlers)
|
|
632
|
+
*/
|
|
633
|
+
_buildGridItemsHTML(pItems, pCursorIndex, pStartIndex)
|
|
634
|
+
{
|
|
635
|
+
let tmpStartIndex = pStartIndex || 0;
|
|
636
|
+
let tmpHTML = '';
|
|
421
637
|
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
422
638
|
let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
|
|
639
|
+
let tmpFmt = this.pict.providers['RetoldRemote-FormattingUtilities'];
|
|
423
640
|
|
|
424
641
|
for (let i = 0; i < pItems.length; i++)
|
|
425
642
|
{
|
|
426
643
|
let tmpItem = pItems[i];
|
|
427
|
-
let
|
|
644
|
+
let tmpAbsoluteIndex = tmpStartIndex + i;
|
|
645
|
+
let tmpSelectedClass = (tmpAbsoluteIndex === pCursorIndex) ? ' selected' : '';
|
|
428
646
|
let tmpExtension = (tmpItem.Extension || '').toLowerCase();
|
|
429
647
|
let tmpCategory = this._getCategory(tmpExtension, tmpItem.Type);
|
|
430
648
|
|
|
431
649
|
tmpHTML += '<div class="retold-remote-tile' + tmpSelectedClass + '" '
|
|
432
|
-
+ 'data-index="' +
|
|
433
|
-
+ 'onclick="pict.views[\'RetoldRemote-Gallery\'].onTileClick(' +
|
|
434
|
-
+ 'ondblclick="pict.views[\'RetoldRemote-Gallery\'].onTileDoubleClick(' +
|
|
650
|
+
+ 'data-index="' + tmpAbsoluteIndex + '" '
|
|
651
|
+
+ 'onclick="pict.views[\'RetoldRemote-Gallery\'].onTileClick(' + tmpAbsoluteIndex + ')" '
|
|
652
|
+
+ 'ondblclick="pict.views[\'RetoldRemote-Gallery\'].onTileDoubleClick(' + tmpAbsoluteIndex + ')">';
|
|
435
653
|
|
|
436
654
|
// Thumbnail area
|
|
437
655
|
tmpHTML += '<div class="retold-remote-tile-thumb">';
|
|
@@ -451,7 +669,7 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
451
669
|
let tmpThumbURL = tmpProvider.getThumbnailURL(tmpItem.Path, 400, 300);
|
|
452
670
|
if (tmpThumbURL)
|
|
453
671
|
{
|
|
454
|
-
tmpHTML += '<img data-src="' + tmpThumbURL + '" alt="' +
|
|
672
|
+
tmpHTML += '<img data-src="' + tmpThumbURL + '" alt="' + tmpFmt.escapeHTML(tmpItem.Name) + '" loading="lazy">';
|
|
455
673
|
}
|
|
456
674
|
else
|
|
457
675
|
{
|
|
@@ -466,7 +684,7 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
466
684
|
let tmpThumbURL = tmpProvider.getThumbnailURL(tmpItem.Path, 400, 300);
|
|
467
685
|
if (tmpThumbURL)
|
|
468
686
|
{
|
|
469
|
-
tmpHTML += '<img data-src="' + tmpThumbURL + '" alt="' +
|
|
687
|
+
tmpHTML += '<img data-src="' + tmpThumbURL + '" alt="' + tmpFmt.escapeHTML(tmpItem.Name) + '" loading="lazy">';
|
|
470
688
|
}
|
|
471
689
|
else
|
|
472
690
|
{
|
|
@@ -497,12 +715,12 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
497
715
|
tmpHTML += '</div>'; // end thumb
|
|
498
716
|
|
|
499
717
|
// Label
|
|
500
|
-
tmpHTML += '<div class="retold-remote-tile-label" title="' +
|
|
718
|
+
tmpHTML += '<div class="retold-remote-tile-label" title="' + tmpFmt.escapeHTML(tmpItem.Name) + '">' + tmpFmt.escapeHTML(tmpItem.Name) + '</div>';
|
|
501
719
|
|
|
502
720
|
// Meta
|
|
503
721
|
if (tmpItem.Type === 'file' && tmpItem.Size !== undefined)
|
|
504
722
|
{
|
|
505
|
-
tmpHTML += '<div class="retold-remote-tile-meta">' +
|
|
723
|
+
tmpHTML += '<div class="retold-remote-tile-meta">' + tmpFmt.formatFileSize(tmpItem.Size) + '</div>';
|
|
506
724
|
}
|
|
507
725
|
else if (tmpItem.Type === 'folder')
|
|
508
726
|
{
|
|
@@ -510,34 +728,51 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
510
728
|
}
|
|
511
729
|
else if (tmpItem.Type === 'archive')
|
|
512
730
|
{
|
|
513
|
-
tmpHTML += '<div class="retold-remote-tile-meta">Archive' + (tmpItem.Size ? '
|
|
731
|
+
tmpHTML += '<div class="retold-remote-tile-meta">Archive' + (tmpItem.Size ? ' \u00b7 ' + tmpFmt.formatFileSize(tmpItem.Size) : '') + '</div>';
|
|
514
732
|
}
|
|
515
733
|
|
|
516
734
|
tmpHTML += '</div>'; // end tile
|
|
517
735
|
}
|
|
518
736
|
|
|
519
|
-
tmpHTML += '</div>'; // end grid
|
|
520
|
-
|
|
521
737
|
return tmpHTML;
|
|
522
738
|
}
|
|
523
739
|
|
|
524
740
|
/**
|
|
525
|
-
* Build the list view HTML.
|
|
741
|
+
* Build the list view HTML — full wrapper + all items.
|
|
742
|
+
* Used by the synchronous fast path for small folders.
|
|
526
743
|
*/
|
|
527
744
|
_buildListHTML(pItems, pCursorIndex)
|
|
528
745
|
{
|
|
746
|
+
let tmpHTML = '<div class="retold-remote-list">';
|
|
747
|
+
tmpHTML += this._buildListItemsHTML(pItems, pCursorIndex, 0);
|
|
748
|
+
tmpHTML += '</div>';
|
|
749
|
+
return tmpHTML;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Build the per-item HTML for a list view slice.
|
|
754
|
+
*
|
|
755
|
+
* @param {Array} pItems - The items to render (may be a slice of the full list)
|
|
756
|
+
* @param {number} pCursorIndex - Index of the currently-selected item in the FULL list
|
|
757
|
+
* @param {number} pStartIndex - Index of pItems[0] within the full list
|
|
758
|
+
*/
|
|
759
|
+
_buildListItemsHTML(pItems, pCursorIndex, pStartIndex)
|
|
760
|
+
{
|
|
761
|
+
let tmpStartIndex = pStartIndex || 0;
|
|
529
762
|
let tmpRemote = this.pict.AppData.RetoldRemote;
|
|
530
763
|
let tmpShowExt = tmpRemote.ListShowExtension !== false;
|
|
531
764
|
let tmpShowSize = tmpRemote.ListShowSize !== false;
|
|
532
765
|
let tmpShowDate = tmpRemote.ListShowDate !== false;
|
|
533
766
|
|
|
534
|
-
let tmpHTML = '
|
|
767
|
+
let tmpHTML = '';
|
|
535
768
|
let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
|
|
769
|
+
let tmpFmt = this.pict.providers['RetoldRemote-FormattingUtilities'];
|
|
536
770
|
|
|
537
771
|
for (let i = 0; i < pItems.length; i++)
|
|
538
772
|
{
|
|
539
773
|
let tmpItem = pItems[i];
|
|
540
|
-
let
|
|
774
|
+
let tmpAbsoluteIndex = tmpStartIndex + i;
|
|
775
|
+
let tmpSelectedClass = (tmpAbsoluteIndex === pCursorIndex) ? ' selected' : '';
|
|
541
776
|
let tmpIcon = '';
|
|
542
777
|
if (tmpIconProvider)
|
|
543
778
|
{
|
|
@@ -545,16 +780,16 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
545
780
|
}
|
|
546
781
|
|
|
547
782
|
tmpHTML += '<div class="retold-remote-list-row' + tmpSelectedClass + '" '
|
|
548
|
-
+ 'data-index="' +
|
|
549
|
-
+ 'onclick="pict.views[\'RetoldRemote-Gallery\'].onTileClick(' +
|
|
550
|
-
+ 'ondblclick="pict.views[\'RetoldRemote-Gallery\'].onTileDoubleClick(' +
|
|
783
|
+
+ 'data-index="' + tmpAbsoluteIndex + '" '
|
|
784
|
+
+ 'onclick="pict.views[\'RetoldRemote-Gallery\'].onTileClick(' + tmpAbsoluteIndex + ')" '
|
|
785
|
+
+ 'ondblclick="pict.views[\'RetoldRemote-Gallery\'].onTileDoubleClick(' + tmpAbsoluteIndex + ')">';
|
|
551
786
|
|
|
552
787
|
tmpHTML += '<div class="retold-remote-list-icon">' + tmpIcon + '</div>';
|
|
553
|
-
tmpHTML += '<div class="retold-remote-list-name" title="' +
|
|
554
|
-
+ ' ontouchstart="pict.views[\'RetoldRemote-Gallery\']._onNameTouchStart(event, \'' +
|
|
788
|
+
tmpHTML += '<div class="retold-remote-list-name" title="' + tmpFmt.escapeHTML(tmpItem.Name) + '"'
|
|
789
|
+
+ ' ontouchstart="pict.views[\'RetoldRemote-Gallery\']._onNameTouchStart(event, \'' + tmpFmt.escapeHTML(tmpItem.Name).replace(/'/g, '\\'') + '\')"'
|
|
555
790
|
+ ' ontouchend="pict.views[\'RetoldRemote-Gallery\']._onNameTouchEnd(event)"'
|
|
556
791
|
+ ' ontouchcancel="pict.views[\'RetoldRemote-Gallery\']._onNameTouchEnd(event)"'
|
|
557
|
-
+ '>' +
|
|
792
|
+
+ '>' + tmpFmt.escapeHTML(tmpItem.Name) + '</div>';
|
|
558
793
|
|
|
559
794
|
// Extension column
|
|
560
795
|
if (tmpShowExt)
|
|
@@ -576,7 +811,7 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
576
811
|
{
|
|
577
812
|
if ((tmpItem.Type === 'file' || tmpItem.Type === 'archive') && tmpItem.Size !== undefined)
|
|
578
813
|
{
|
|
579
|
-
tmpHTML += '<div class="retold-remote-list-size">' +
|
|
814
|
+
tmpHTML += '<div class="retold-remote-list-size">' + tmpFmt.formatFileSize(tmpItem.Size) + '</div>';
|
|
580
815
|
}
|
|
581
816
|
else
|
|
582
817
|
{
|
|
@@ -589,7 +824,7 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
589
824
|
{
|
|
590
825
|
if (tmpItem.Modified)
|
|
591
826
|
{
|
|
592
|
-
tmpHTML += '<div class="retold-remote-list-date">' +
|
|
827
|
+
tmpHTML += '<div class="retold-remote-list-date">' + tmpFmt.formatShortDate(tmpItem.Modified) + '</div>';
|
|
593
828
|
}
|
|
594
829
|
else
|
|
595
830
|
{
|
|
@@ -600,8 +835,6 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
600
835
|
tmpHTML += '</div>';
|
|
601
836
|
}
|
|
602
837
|
|
|
603
|
-
tmpHTML += '</div>';
|
|
604
|
-
|
|
605
838
|
return tmpHTML;
|
|
606
839
|
}
|
|
607
840
|
|
|
@@ -611,6 +844,8 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
611
844
|
|
|
612
845
|
/**
|
|
613
846
|
* Set up IntersectionObserver for lazy-loading thumbnail images.
|
|
847
|
+
* Uses a bounded concurrency queue so we don't stampede the server
|
|
848
|
+
* with hundreds of parallel thumbnail requests on the initial render.
|
|
614
849
|
*/
|
|
615
850
|
_setupLazyLoading()
|
|
616
851
|
{
|
|
@@ -619,12 +854,13 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
619
854
|
this._intersectionObserver.disconnect();
|
|
620
855
|
}
|
|
621
856
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
857
|
+
// Reset the concurrency queue. Any in-flight loads from the previous
|
|
858
|
+
// render will finish on their own; they just won't trigger queue drain
|
|
859
|
+
// because the counter is reset.
|
|
860
|
+
this._thumbnailQueue = [];
|
|
861
|
+
this._thumbnailInFlight = 0;
|
|
627
862
|
|
|
863
|
+
let tmpSelf = this;
|
|
628
864
|
this._intersectionObserver = new IntersectionObserver(
|
|
629
865
|
(pEntries) =>
|
|
630
866
|
{
|
|
@@ -633,21 +869,86 @@ class RetoldRemoteGalleryView extends libPictView
|
|
|
633
869
|
if (pEntries[i].isIntersecting)
|
|
634
870
|
{
|
|
635
871
|
let tmpImg = pEntries[i].target;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
this._intersectionObserver.unobserve(tmpImg);
|
|
872
|
+
tmpSelf._intersectionObserver.unobserve(tmpImg);
|
|
873
|
+
tmpSelf._enqueueThumbnail(tmpImg);
|
|
639
874
|
}
|
|
640
875
|
}
|
|
641
876
|
},
|
|
642
|
-
{ rootMargin: '
|
|
877
|
+
{ rootMargin: '100px' }
|
|
643
878
|
);
|
|
644
879
|
|
|
880
|
+
// Observe every img[data-src] currently in the DOM. Chunked renders
|
|
881
|
+
// also call _observeNewThumbnails() incrementally so thumbnails start
|
|
882
|
+
// loading while later chunks are still being built.
|
|
883
|
+
this._observeNewThumbnails();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Observe any img[data-src] nodes that are not yet being watched by
|
|
888
|
+
* the IntersectionObserver. Called incrementally from the chunked
|
|
889
|
+
* render after each chunk is appended, so visible thumbnails in the
|
|
890
|
+
* first chunk can start loading while later chunks render.
|
|
891
|
+
*/
|
|
892
|
+
_observeNewThumbnails()
|
|
893
|
+
{
|
|
894
|
+
if (!this._intersectionObserver)
|
|
895
|
+
{
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
// Query only images still carrying data-src (new ones from the latest
|
|
899
|
+
// chunk, or ones that haven't intersected yet)
|
|
900
|
+
let tmpImages = document.querySelectorAll('.retold-remote-tile-thumb img[data-src]:not([data-observed])');
|
|
645
901
|
for (let i = 0; i < tmpImages.length; i++)
|
|
646
902
|
{
|
|
903
|
+
tmpImages[i].setAttribute('data-observed', '1');
|
|
647
904
|
this._intersectionObserver.observe(tmpImages[i]);
|
|
648
905
|
}
|
|
649
906
|
}
|
|
650
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Enqueue a thumbnail for lazy-loading with bounded concurrency.
|
|
910
|
+
* Dispatches immediately if under the concurrency cap.
|
|
911
|
+
*
|
|
912
|
+
* @param {HTMLImageElement} pImg - The img element to load
|
|
913
|
+
*/
|
|
914
|
+
_enqueueThumbnail(pImg)
|
|
915
|
+
{
|
|
916
|
+
this._thumbnailQueue.push(pImg);
|
|
917
|
+
this._drainThumbnailQueue();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Dispatch pending thumbnails until the in-flight count hits the cap.
|
|
922
|
+
*/
|
|
923
|
+
_drainThumbnailQueue()
|
|
924
|
+
{
|
|
925
|
+
let tmpSelf = this;
|
|
926
|
+
while (this._thumbnailInFlight < _MAX_THUMBNAIL_CONCURRENCY && this._thumbnailQueue.length > 0)
|
|
927
|
+
{
|
|
928
|
+
let tmpImg = this._thumbnailQueue.shift();
|
|
929
|
+
let tmpSrc = tmpImg.getAttribute('data-src');
|
|
930
|
+
if (!tmpSrc)
|
|
931
|
+
{
|
|
932
|
+
// Already loaded or cleared — skip
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
this._thumbnailInFlight++;
|
|
937
|
+
|
|
938
|
+
let _finish = function ()
|
|
939
|
+
{
|
|
940
|
+
tmpSelf._thumbnailInFlight--;
|
|
941
|
+
tmpSelf._drainThumbnailQueue();
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
tmpImg.addEventListener('load', _finish, { once: true });
|
|
945
|
+
tmpImg.addEventListener('error', _finish, { once: true });
|
|
946
|
+
|
|
947
|
+
tmpImg.src = tmpSrc;
|
|
948
|
+
tmpImg.removeAttribute('data-src');
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
651
952
|
// ──────────────────────────────────────────────
|
|
652
953
|
// Event handlers
|
|
653
954
|
// ──────────────────────────────────────────────
|