retold-remote 0.0.4 → 0.0.6
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/docs/README.md +181 -0
- package/docs/_cover.md +14 -0
- package/docs/_sidebar.md +10 -0
- package/docs/_topbar.md +3 -0
- package/docs/audio-viewer.md +133 -0
- package/docs/ebook-reader.md +90 -0
- package/docs/image-viewer.md +90 -0
- package/docs/server-setup.md +262 -0
- package/docs/video-viewer.md +134 -0
- package/html/docs.html +59 -0
- package/package.json +21 -7
- package/source/Pict-Application-RetoldRemote.js +143 -2
- package/source/RetoldRemote-ExtensionMaps.js +33 -0
- package/source/cli/RetoldRemote-Server-Setup.js +82 -67
- package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
- package/source/providers/Pict-Provider-CollectionManager.js +934 -0
- package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
- package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
- package/source/providers/Pict-Provider-GalleryNavigation.js +270 -353
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
- package/source/providers/Pict-Provider-ToastNotification.js +96 -0
- package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
- package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
- package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
- package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
- package/source/server/RetoldRemote-ArchiveService.js +2 -12
- package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
- package/source/server/RetoldRemote-CollectionService.js +684 -0
- package/source/server/RetoldRemote-EbookService.js +7 -16
- package/source/server/RetoldRemote-MediaService.js +3 -14
- package/source/server/RetoldRemote-ParimeCache.js +349 -0
- package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
- package/source/server/RetoldRemote-VideoFrameService.js +7 -15
- package/source/views/PictView-Remote-AudioExplorer.js +10 -43
- package/source/views/PictView-Remote-CollectionsPanel.js +1087 -0
- package/source/views/PictView-Remote-Gallery.js +237 -44
- package/source/views/PictView-Remote-ImageViewer.js +1 -34
- package/source/views/PictView-Remote-Layout.js +410 -20
- package/source/views/PictView-Remote-MediaViewer.js +338 -51
- package/source/views/PictView-Remote-SettingsPanel.js +155 -138
- package/source/views/PictView-Remote-TopBar.js +615 -14
- package/source/views/PictView-Remote-VLCSetup.js +766 -0
- package/source/views/PictView-Remote-VideoExplorer.js +20 -54
- package/web-application/css/docuserve.css +73 -0
- package/web-application/docs/README.md +181 -0
- package/web-application/docs/_cover.md +14 -0
- package/web-application/docs/_sidebar.md +10 -0
- package/web-application/docs/_topbar.md +3 -0
- package/web-application/docs/audio-viewer.md +133 -0
- package/web-application/docs/ebook-reader.md +90 -0
- package/web-application/docs/image-viewer.md +90 -0
- package/web-application/docs/server-setup.md +262 -0
- package/web-application/docs/video-viewer.md +134 -0
- package/web-application/docs.html +59 -0
- package/web-application/js/pict-docuserve.min.js +58 -0
- 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 +2558 -439
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +41 -11
- package/web-application/retold-remote.min.js.map +1 -1
- package/server.js +0 -43
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Remote -- Collection Manager Provider
|
|
3
|
+
*
|
|
4
|
+
* Client-side state management and API communication for the
|
|
5
|
+
* collections feature. Provides methods for CRUD operations on
|
|
6
|
+
* collections and their items, plus panel state management.
|
|
7
|
+
*
|
|
8
|
+
* All collection state lives on pict.AppData.RetoldRemote and is
|
|
9
|
+
* mutated directly followed by explicit render calls -- matching the
|
|
10
|
+
* existing retold-remote state management pattern.
|
|
11
|
+
*
|
|
12
|
+
* @license MIT
|
|
13
|
+
*/
|
|
14
|
+
const libPictProvider = require('pict-provider');
|
|
15
|
+
|
|
16
|
+
const _DefaultProviderConfiguration =
|
|
17
|
+
{
|
|
18
|
+
ProviderIdentifier: 'RetoldRemote-CollectionManager',
|
|
19
|
+
AutoInitialize: true,
|
|
20
|
+
AutoSolveWithApp: false
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
class CollectionManagerProvider extends libPictProvider
|
|
24
|
+
{
|
|
25
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
26
|
+
{
|
|
27
|
+
super(pFable, pOptions, pServiceHash);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// -- State Accessors --------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Shortcut to the RetoldRemote AppData namespace.
|
|
34
|
+
*/
|
|
35
|
+
_getRemote()
|
|
36
|
+
{
|
|
37
|
+
return this.pict.AppData.RetoldRemote;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the collections panel view.
|
|
42
|
+
*/
|
|
43
|
+
_getPanelView()
|
|
44
|
+
{
|
|
45
|
+
return this.pict.views['RetoldRemote-CollectionsPanel'];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the toast notification provider.
|
|
50
|
+
*/
|
|
51
|
+
_getToast()
|
|
52
|
+
{
|
|
53
|
+
return this.pict.providers['RetoldRemote-ToastNotification'];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// -- API Methods ------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fetch all collections (summaries) from the server.
|
|
60
|
+
*
|
|
61
|
+
* @param {Function} [fCallback] - Optional callback(pError, pCollections)
|
|
62
|
+
*/
|
|
63
|
+
fetchCollections(fCallback)
|
|
64
|
+
{
|
|
65
|
+
let tmpSelf = this;
|
|
66
|
+
let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
|
|
67
|
+
|
|
68
|
+
fetch('/api/collections')
|
|
69
|
+
.then((pResponse) => pResponse.json())
|
|
70
|
+
.then((pData) =>
|
|
71
|
+
{
|
|
72
|
+
let tmpRemote = tmpSelf._getRemote();
|
|
73
|
+
tmpRemote.Collections = Array.isArray(pData) ? pData : [];
|
|
74
|
+
|
|
75
|
+
let tmpPanel = tmpSelf._getPanelView();
|
|
76
|
+
if (tmpPanel && tmpRemote.CollectionsPanelOpen)
|
|
77
|
+
{
|
|
78
|
+
tmpPanel.renderContent();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return tmpCallback(null, tmpRemote.Collections);
|
|
82
|
+
})
|
|
83
|
+
.catch((pError) =>
|
|
84
|
+
{
|
|
85
|
+
tmpSelf.log.error('Failed to fetch collections: ' + pError.message);
|
|
86
|
+
return tmpCallback(pError);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Fetch a single collection with all its items.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} pGUID - Collection GUID
|
|
94
|
+
* @param {Function} [fCallback] - Optional callback(pError, pCollection)
|
|
95
|
+
*/
|
|
96
|
+
fetchCollection(pGUID, fCallback)
|
|
97
|
+
{
|
|
98
|
+
let tmpSelf = this;
|
|
99
|
+
let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
|
|
100
|
+
|
|
101
|
+
fetch('/api/collections/' + encodeURIComponent(pGUID))
|
|
102
|
+
.then((pResponse) =>
|
|
103
|
+
{
|
|
104
|
+
if (!pResponse.ok)
|
|
105
|
+
{
|
|
106
|
+
throw new Error('Collection not found');
|
|
107
|
+
}
|
|
108
|
+
return pResponse.json();
|
|
109
|
+
})
|
|
110
|
+
.then((pData) =>
|
|
111
|
+
{
|
|
112
|
+
let tmpRemote = tmpSelf._getRemote();
|
|
113
|
+
tmpRemote.ActiveCollectionGUID = pGUID;
|
|
114
|
+
tmpRemote.ActiveCollection = pData;
|
|
115
|
+
|
|
116
|
+
let tmpPanel = tmpSelf._getPanelView();
|
|
117
|
+
if (tmpPanel)
|
|
118
|
+
{
|
|
119
|
+
tmpPanel.renderContent();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return tmpCallback(null, pData);
|
|
123
|
+
})
|
|
124
|
+
.catch((pError) =>
|
|
125
|
+
{
|
|
126
|
+
tmpSelf.log.error('Failed to fetch collection: ' + pError.message);
|
|
127
|
+
return tmpCallback(pError);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a new collection.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} pName - Collection name
|
|
135
|
+
* @param {Function} [fCallback] - Optional callback(pError, pCollection)
|
|
136
|
+
*/
|
|
137
|
+
createCollection(pName, fCallback)
|
|
138
|
+
{
|
|
139
|
+
let tmpSelf = this;
|
|
140
|
+
let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
|
|
141
|
+
let tmpGUID = this.fable.getUUID();
|
|
142
|
+
|
|
143
|
+
fetch('/api/collections/' + encodeURIComponent(tmpGUID),
|
|
144
|
+
{
|
|
145
|
+
method: 'PUT',
|
|
146
|
+
headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({ Name: pName || 'Untitled Collection' })
|
|
148
|
+
})
|
|
149
|
+
.then((pResponse) => pResponse.json())
|
|
150
|
+
.then((pData) =>
|
|
151
|
+
{
|
|
152
|
+
let tmpRemote = tmpSelf._getRemote();
|
|
153
|
+
tmpRemote.LastUsedCollectionGUID = tmpGUID;
|
|
154
|
+
|
|
155
|
+
// Refresh the list
|
|
156
|
+
tmpSelf.fetchCollections();
|
|
157
|
+
|
|
158
|
+
let tmpToast = tmpSelf._getToast();
|
|
159
|
+
if (tmpToast)
|
|
160
|
+
{
|
|
161
|
+
tmpToast.show('Collection created: ' + (pData.Name || pName));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return tmpCallback(null, pData);
|
|
165
|
+
})
|
|
166
|
+
.catch((pError) =>
|
|
167
|
+
{
|
|
168
|
+
tmpSelf.log.error('Failed to create collection: ' + pError.message);
|
|
169
|
+
return tmpCallback(pError);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Update an existing collection's metadata.
|
|
175
|
+
*
|
|
176
|
+
* @param {object} pCollection - Collection object with GUID and updated fields
|
|
177
|
+
* @param {Function} [fCallback] - Optional callback(pError, pCollection)
|
|
178
|
+
*/
|
|
179
|
+
updateCollection(pCollection, fCallback)
|
|
180
|
+
{
|
|
181
|
+
let tmpSelf = this;
|
|
182
|
+
let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
|
|
183
|
+
|
|
184
|
+
if (!pCollection || !pCollection.GUID)
|
|
185
|
+
{
|
|
186
|
+
return tmpCallback(new Error('Collection must have a GUID'));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fetch('/api/collections/' + encodeURIComponent(pCollection.GUID),
|
|
190
|
+
{
|
|
191
|
+
method: 'PUT',
|
|
192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify(pCollection)
|
|
194
|
+
})
|
|
195
|
+
.then((pResponse) => pResponse.json())
|
|
196
|
+
.then((pData) =>
|
|
197
|
+
{
|
|
198
|
+
let tmpRemote = tmpSelf._getRemote();
|
|
199
|
+
|
|
200
|
+
// Update in-memory active collection if it's the same one
|
|
201
|
+
if (tmpRemote.ActiveCollectionGUID === pCollection.GUID)
|
|
202
|
+
{
|
|
203
|
+
tmpRemote.ActiveCollection = pData;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Refresh the list
|
|
207
|
+
tmpSelf.fetchCollections();
|
|
208
|
+
|
|
209
|
+
return tmpCallback(null, pData);
|
|
210
|
+
})
|
|
211
|
+
.catch((pError) =>
|
|
212
|
+
{
|
|
213
|
+
tmpSelf.log.error('Failed to update collection: ' + pError.message);
|
|
214
|
+
return tmpCallback(pError);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Delete a collection.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} pGUID - Collection GUID
|
|
222
|
+
* @param {Function} [fCallback] - Optional callback(pError)
|
|
223
|
+
*/
|
|
224
|
+
deleteCollection(pGUID, fCallback)
|
|
225
|
+
{
|
|
226
|
+
let tmpSelf = this;
|
|
227
|
+
let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
|
|
228
|
+
|
|
229
|
+
fetch('/api/collections/' + encodeURIComponent(pGUID),
|
|
230
|
+
{
|
|
231
|
+
method: 'DELETE'
|
|
232
|
+
})
|
|
233
|
+
.then((pResponse) => pResponse.json())
|
|
234
|
+
.then(() =>
|
|
235
|
+
{
|
|
236
|
+
let tmpRemote = tmpSelf._getRemote();
|
|
237
|
+
|
|
238
|
+
// If we deleted the active collection, go back to list
|
|
239
|
+
if (tmpRemote.ActiveCollectionGUID === pGUID)
|
|
240
|
+
{
|
|
241
|
+
tmpRemote.ActiveCollectionGUID = null;
|
|
242
|
+
tmpRemote.ActiveCollection = null;
|
|
243
|
+
tmpRemote.CollectionsPanelMode = 'list';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// If we deleted the last-used collection, clear it
|
|
247
|
+
if (tmpRemote.LastUsedCollectionGUID === pGUID)
|
|
248
|
+
{
|
|
249
|
+
tmpRemote.LastUsedCollectionGUID = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Refresh the list
|
|
253
|
+
tmpSelf.fetchCollections();
|
|
254
|
+
|
|
255
|
+
let tmpToast = tmpSelf._getToast();
|
|
256
|
+
if (tmpToast)
|
|
257
|
+
{
|
|
258
|
+
tmpToast.show('Collection deleted');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return tmpCallback(null);
|
|
262
|
+
})
|
|
263
|
+
.catch((pError) =>
|
|
264
|
+
{
|
|
265
|
+
tmpSelf.log.error('Failed to delete collection: ' + pError.message);
|
|
266
|
+
return tmpCallback(pError);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Add item(s) to a collection.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} pGUID - Collection GUID
|
|
274
|
+
* @param {Array} pItems - Array of item objects (each with Type, Path, etc.)
|
|
275
|
+
* @param {Function} [fCallback] - Optional callback(pError, pCollection)
|
|
276
|
+
*/
|
|
277
|
+
addItemsToCollection(pGUID, pItems, fCallback)
|
|
278
|
+
{
|
|
279
|
+
let tmpSelf = this;
|
|
280
|
+
let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
|
|
281
|
+
|
|
282
|
+
fetch('/api/collections/' + encodeURIComponent(pGUID) + '/items',
|
|
283
|
+
{
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: { 'Content-Type': 'application/json' },
|
|
286
|
+
body: JSON.stringify({ Items: pItems })
|
|
287
|
+
})
|
|
288
|
+
.then((pResponse) => pResponse.json())
|
|
289
|
+
.then((pData) =>
|
|
290
|
+
{
|
|
291
|
+
let tmpRemote = tmpSelf._getRemote();
|
|
292
|
+
tmpRemote.LastUsedCollectionGUID = pGUID;
|
|
293
|
+
|
|
294
|
+
// Update in-memory active collection if it's the same one
|
|
295
|
+
if (tmpRemote.ActiveCollectionGUID === pGUID)
|
|
296
|
+
{
|
|
297
|
+
tmpRemote.ActiveCollection = pData;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Refresh the summary list
|
|
301
|
+
tmpSelf.fetchCollections();
|
|
302
|
+
|
|
303
|
+
let tmpPanel = tmpSelf._getPanelView();
|
|
304
|
+
if (tmpPanel && tmpRemote.CollectionsPanelMode === 'detail' && tmpRemote.ActiveCollectionGUID === pGUID)
|
|
305
|
+
{
|
|
306
|
+
tmpPanel.renderContent();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let tmpToast = tmpSelf._getToast();
|
|
310
|
+
if (tmpToast)
|
|
311
|
+
{
|
|
312
|
+
let tmpCollectionName = pData.Name || 'collection';
|
|
313
|
+
tmpToast.show('Added ' + pItems.length + ' item' + (pItems.length > 1 ? 's' : '') + ' to ' + tmpCollectionName);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return tmpCallback(null, pData);
|
|
317
|
+
})
|
|
318
|
+
.catch((pError) =>
|
|
319
|
+
{
|
|
320
|
+
tmpSelf.log.error('Failed to add items to collection: ' + pError.message);
|
|
321
|
+
return tmpCallback(pError);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Remove an item from a collection.
|
|
327
|
+
*
|
|
328
|
+
* @param {string} pGUID - Collection GUID
|
|
329
|
+
* @param {string} pItemID - Item ID within the collection
|
|
330
|
+
* @param {Function} [fCallback] - Optional callback(pError, pCollection)
|
|
331
|
+
*/
|
|
332
|
+
removeItemFromCollection(pGUID, pItemID, fCallback)
|
|
333
|
+
{
|
|
334
|
+
let tmpSelf = this;
|
|
335
|
+
let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
|
|
336
|
+
|
|
337
|
+
fetch('/api/collections/' + encodeURIComponent(pGUID) + '/items/' + encodeURIComponent(pItemID),
|
|
338
|
+
{
|
|
339
|
+
method: 'DELETE'
|
|
340
|
+
})
|
|
341
|
+
.then((pResponse) => pResponse.json())
|
|
342
|
+
.then((pData) =>
|
|
343
|
+
{
|
|
344
|
+
let tmpRemote = tmpSelf._getRemote();
|
|
345
|
+
|
|
346
|
+
// Update in-memory active collection if it's the same one
|
|
347
|
+
if (tmpRemote.ActiveCollectionGUID === pGUID)
|
|
348
|
+
{
|
|
349
|
+
tmpRemote.ActiveCollection = pData;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Refresh the summary list
|
|
353
|
+
tmpSelf.fetchCollections();
|
|
354
|
+
|
|
355
|
+
let tmpPanel = tmpSelf._getPanelView();
|
|
356
|
+
if (tmpPanel && tmpRemote.CollectionsPanelMode === 'detail' && tmpRemote.ActiveCollectionGUID === pGUID)
|
|
357
|
+
{
|
|
358
|
+
tmpPanel.renderContent();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return tmpCallback(null, pData);
|
|
362
|
+
})
|
|
363
|
+
.catch((pError) =>
|
|
364
|
+
{
|
|
365
|
+
tmpSelf.log.error('Failed to remove item from collection: ' + pError.message);
|
|
366
|
+
return tmpCallback(pError);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Reorder items in a collection (manual sort).
|
|
372
|
+
*
|
|
373
|
+
* @param {string} pGUID - Collection GUID
|
|
374
|
+
* @param {Array} pItemOrder - Array of item IDs in desired order
|
|
375
|
+
* @param {Function} [fCallback] - Optional callback(pError, pCollection)
|
|
376
|
+
*/
|
|
377
|
+
reorderItems(pGUID, pItemOrder, fCallback)
|
|
378
|
+
{
|
|
379
|
+
let tmpSelf = this;
|
|
380
|
+
let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
|
|
381
|
+
|
|
382
|
+
fetch('/api/collections/' + encodeURIComponent(pGUID) + '/reorder',
|
|
383
|
+
{
|
|
384
|
+
method: 'PUT',
|
|
385
|
+
headers: { 'Content-Type': 'application/json' },
|
|
386
|
+
body: JSON.stringify({ ItemOrder: pItemOrder })
|
|
387
|
+
})
|
|
388
|
+
.then((pResponse) => pResponse.json())
|
|
389
|
+
.then((pData) =>
|
|
390
|
+
{
|
|
391
|
+
let tmpRemote = tmpSelf._getRemote();
|
|
392
|
+
|
|
393
|
+
if (tmpRemote.ActiveCollectionGUID === pGUID)
|
|
394
|
+
{
|
|
395
|
+
tmpRemote.ActiveCollection = pData;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let tmpPanel = tmpSelf._getPanelView();
|
|
399
|
+
if (tmpPanel && tmpRemote.CollectionsPanelMode === 'detail')
|
|
400
|
+
{
|
|
401
|
+
tmpPanel.renderContent();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return tmpCallback(null, pData);
|
|
405
|
+
})
|
|
406
|
+
.catch((pError) =>
|
|
407
|
+
{
|
|
408
|
+
tmpSelf.log.error('Failed to reorder items: ' + pError.message);
|
|
409
|
+
return tmpCallback(pError);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Copy items from one collection to another.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} pSourceGUID - Source collection GUID
|
|
417
|
+
* @param {string} pTargetGUID - Target collection GUID
|
|
418
|
+
* @param {Array} pItemIDs - Array of item IDs to copy
|
|
419
|
+
* @param {Function} [fCallback] - Optional callback(pError, pTargetCollection)
|
|
420
|
+
*/
|
|
421
|
+
copyItems(pSourceGUID, pTargetGUID, pItemIDs, fCallback)
|
|
422
|
+
{
|
|
423
|
+
let tmpSelf = this;
|
|
424
|
+
let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
|
|
425
|
+
|
|
426
|
+
fetch('/api/collections/copy-items',
|
|
427
|
+
{
|
|
428
|
+
method: 'POST',
|
|
429
|
+
headers: { 'Content-Type': 'application/json' },
|
|
430
|
+
body: JSON.stringify(
|
|
431
|
+
{
|
|
432
|
+
SourceGUID: pSourceGUID,
|
|
433
|
+
TargetGUID: pTargetGUID,
|
|
434
|
+
ItemIDs: pItemIDs
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
.then((pResponse) => pResponse.json())
|
|
438
|
+
.then((pData) =>
|
|
439
|
+
{
|
|
440
|
+
// Refresh the list
|
|
441
|
+
tmpSelf.fetchCollections();
|
|
442
|
+
|
|
443
|
+
let tmpToast = tmpSelf._getToast();
|
|
444
|
+
if (tmpToast)
|
|
445
|
+
{
|
|
446
|
+
tmpToast.show('Copied ' + pItemIDs.length + ' item' + (pItemIDs.length > 1 ? 's' : ''));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return tmpCallback(null, pData);
|
|
450
|
+
})
|
|
451
|
+
.catch((pError) =>
|
|
452
|
+
{
|
|
453
|
+
tmpSelf.log.error('Failed to copy items: ' + pError.message);
|
|
454
|
+
return tmpCallback(pError);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// -- Panel State Methods ----------------------------------------------
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Toggle the collections panel open/closed.
|
|
462
|
+
*/
|
|
463
|
+
togglePanel()
|
|
464
|
+
{
|
|
465
|
+
let tmpRemote = this._getRemote();
|
|
466
|
+
tmpRemote.CollectionsPanelOpen = !tmpRemote.CollectionsPanelOpen;
|
|
467
|
+
|
|
468
|
+
let tmpWrap = document.getElementById('RetoldRemote-Collections-Wrap');
|
|
469
|
+
if (tmpWrap)
|
|
470
|
+
{
|
|
471
|
+
if (tmpRemote.CollectionsPanelOpen)
|
|
472
|
+
{
|
|
473
|
+
tmpWrap.classList.remove('collapsed');
|
|
474
|
+
// Restore saved width
|
|
475
|
+
if (tmpRemote.CollectionsPanelWidth)
|
|
476
|
+
{
|
|
477
|
+
tmpWrap.style.width = tmpRemote.CollectionsPanelWidth + 'px';
|
|
478
|
+
}
|
|
479
|
+
// Fetch latest collections when opening
|
|
480
|
+
this.fetchCollections();
|
|
481
|
+
}
|
|
482
|
+
else
|
|
483
|
+
{
|
|
484
|
+
tmpWrap.classList.add('collapsed');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Update topbar button state
|
|
489
|
+
let tmpTopBar = this.pict.views['ContentEditor-TopBar'];
|
|
490
|
+
if (tmpTopBar && typeof tmpTopBar.updateCollectionsIcon === 'function')
|
|
491
|
+
{
|
|
492
|
+
tmpTopBar.updateCollectionsIcon();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Persist setting
|
|
496
|
+
this.pict.PictApplication.saveSettings();
|
|
497
|
+
|
|
498
|
+
// Recalculate gallery columns after panel animation
|
|
499
|
+
let tmpSelf = this;
|
|
500
|
+
setTimeout(() =>
|
|
501
|
+
{
|
|
502
|
+
let tmpGalleryNav = tmpSelf.pict.providers['RetoldRemote-GalleryNavigation'];
|
|
503
|
+
if (tmpGalleryNav && typeof tmpGalleryNav.recalculateColumns === 'function')
|
|
504
|
+
{
|
|
505
|
+
tmpGalleryNav.recalculateColumns();
|
|
506
|
+
}
|
|
507
|
+
}, 250);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Open the collections panel.
|
|
512
|
+
*/
|
|
513
|
+
openPanel()
|
|
514
|
+
{
|
|
515
|
+
let tmpRemote = this._getRemote();
|
|
516
|
+
if (!tmpRemote.CollectionsPanelOpen)
|
|
517
|
+
{
|
|
518
|
+
this.togglePanel();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Close the collections panel.
|
|
524
|
+
*/
|
|
525
|
+
closePanel()
|
|
526
|
+
{
|
|
527
|
+
let tmpRemote = this._getRemote();
|
|
528
|
+
if (tmpRemote.CollectionsPanelOpen)
|
|
529
|
+
{
|
|
530
|
+
this.togglePanel();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// -- Convenience Methods ----------------------------------------------
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Add the currently viewed file to a collection.
|
|
538
|
+
* If pGUID is not provided, uses the last-used collection.
|
|
539
|
+
*
|
|
540
|
+
* @param {string} [pGUID] - Collection GUID (omit for quick-add to last-used)
|
|
541
|
+
* @returns {boolean} true if the add was initiated
|
|
542
|
+
*/
|
|
543
|
+
addCurrentFileToCollection(pGUID)
|
|
544
|
+
{
|
|
545
|
+
let tmpRemote = this._getRemote();
|
|
546
|
+
let tmpFilePath = this.pict.AppData.ContentEditor.CurrentFile;
|
|
547
|
+
|
|
548
|
+
if (!tmpFilePath)
|
|
549
|
+
{
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let tmpTargetGUID = pGUID || tmpRemote.LastUsedCollectionGUID;
|
|
554
|
+
if (!tmpTargetGUID)
|
|
555
|
+
{
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Build the item — detect archive subfiles and video timestamp context
|
|
560
|
+
let tmpItem =
|
|
561
|
+
{
|
|
562
|
+
Type: 'file',
|
|
563
|
+
Path: tmpFilePath,
|
|
564
|
+
Label: '',
|
|
565
|
+
Note: ''
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// Detect archive subfile — path contains an archive extension followed by /
|
|
569
|
+
let tmpArchiveMatch = tmpFilePath.match(/^(.*?\.(zip|7z|rar|tar|tgz|cbz|cbr|tar\.gz|tar\.bz2|tar\.xz))\/(.*)/i);
|
|
570
|
+
if (tmpArchiveMatch)
|
|
571
|
+
{
|
|
572
|
+
tmpItem.Type = 'subfile';
|
|
573
|
+
tmpItem.ArchivePath = tmpArchiveMatch[1];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// If we're viewing a video with the player active, capture current timestamp as video-frame
|
|
577
|
+
if (tmpRemote.ActiveMode === 'viewer' && tmpRemote.CurrentViewerMediaType === 'video' && !tmpRemote.VideoMenuActive)
|
|
578
|
+
{
|
|
579
|
+
let tmpVideo = document.getElementById('RetoldRemote-VideoPlayer');
|
|
580
|
+
if (tmpVideo && !isNaN(tmpVideo.currentTime) && tmpVideo.currentTime > 0)
|
|
581
|
+
{
|
|
582
|
+
tmpItem.Type = 'video-frame';
|
|
583
|
+
tmpItem.FrameTimestamp = Math.round(tmpVideo.currentTime * 100) / 100;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// If we have a hash for this file, include it
|
|
588
|
+
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
589
|
+
if (tmpProvider)
|
|
590
|
+
{
|
|
591
|
+
let tmpHash = tmpProvider.getHashForPath(tmpFilePath);
|
|
592
|
+
if (tmpHash)
|
|
593
|
+
{
|
|
594
|
+
tmpItem.Hash = tmpHash;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
this.addItemsToCollection(tmpTargetGUID, [tmpItem]);
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Add a video frame from the video explorer to a collection.
|
|
604
|
+
*
|
|
605
|
+
* @param {string} pGUID - Collection GUID
|
|
606
|
+
* @returns {boolean} true if the add was initiated
|
|
607
|
+
*/
|
|
608
|
+
addVideoFrameToCollection(pGUID)
|
|
609
|
+
{
|
|
610
|
+
let tmpRemote = this._getRemote();
|
|
611
|
+
let tmpTargetGUID = pGUID || tmpRemote.LastUsedCollectionGUID;
|
|
612
|
+
if (!tmpTargetGUID)
|
|
613
|
+
{
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
let tmpVEX = this.pict.views['RetoldRemote-VideoExplorer'];
|
|
618
|
+
if (!tmpVEX || !tmpVEX._currentPath || !tmpVEX._frameData)
|
|
619
|
+
{
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let tmpFrameIndex = tmpVEX._selectedFrameIndex;
|
|
624
|
+
if (tmpFrameIndex < 0)
|
|
625
|
+
{
|
|
626
|
+
// No frame selected — use first frame
|
|
627
|
+
tmpFrameIndex = 0;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let tmpFrame = tmpVEX._frameData.Frames[tmpFrameIndex];
|
|
631
|
+
if (!tmpFrame)
|
|
632
|
+
{
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
let tmpItem =
|
|
637
|
+
{
|
|
638
|
+
Type: 'video-frame',
|
|
639
|
+
Path: tmpVEX._currentPath,
|
|
640
|
+
FrameTimestamp: tmpFrame.Timestamp,
|
|
641
|
+
Label: tmpFrame.TimestampFormatted || '',
|
|
642
|
+
Note: ''
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
this.addItemsToCollection(tmpTargetGUID, [tmpItem]);
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Add a video clip (time range) to a collection from the video player.
|
|
651
|
+
*
|
|
652
|
+
* Uses the current playback position and a duration offset, or explicit
|
|
653
|
+
* start/end timestamps.
|
|
654
|
+
*
|
|
655
|
+
* @param {string} pGUID - Collection GUID
|
|
656
|
+
* @param {number} pStartTime - Start time in seconds
|
|
657
|
+
* @param {number} pEndTime - End time in seconds
|
|
658
|
+
* @returns {boolean} true if the add was initiated
|
|
659
|
+
*/
|
|
660
|
+
addVideoClipToCollection(pGUID, pStartTime, pEndTime)
|
|
661
|
+
{
|
|
662
|
+
let tmpRemote = this._getRemote();
|
|
663
|
+
let tmpTargetGUID = pGUID || tmpRemote.LastUsedCollectionGUID;
|
|
664
|
+
if (!tmpTargetGUID)
|
|
665
|
+
{
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let tmpFilePath = tmpRemote.CurrentViewerFile;
|
|
670
|
+
if (!tmpFilePath)
|
|
671
|
+
{
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
let tmpItem =
|
|
676
|
+
{
|
|
677
|
+
Type: 'video-clip',
|
|
678
|
+
Path: tmpFilePath,
|
|
679
|
+
VideoStart: pStartTime,
|
|
680
|
+
VideoEnd: pEndTime,
|
|
681
|
+
Label: this._formatTimestamp(pStartTime) + ' - ' + this._formatTimestamp(pEndTime),
|
|
682
|
+
Note: ''
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
this.addItemsToCollection(tmpTargetGUID, [tmpItem]);
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Add an audio snippet (selected range) from the audio explorer.
|
|
691
|
+
*
|
|
692
|
+
* @param {string} pGUID - Collection GUID
|
|
693
|
+
* @returns {boolean} true if the add was initiated
|
|
694
|
+
*/
|
|
695
|
+
addAudioSnippetToCollection(pGUID)
|
|
696
|
+
{
|
|
697
|
+
let tmpRemote = this._getRemote();
|
|
698
|
+
let tmpTargetGUID = pGUID || tmpRemote.LastUsedCollectionGUID;
|
|
699
|
+
if (!tmpTargetGUID)
|
|
700
|
+
{
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
let tmpAEX = this.pict.views['RetoldRemote-AudioExplorer'];
|
|
705
|
+
if (!tmpAEX || !tmpAEX._currentPath || !tmpAEX._waveformData)
|
|
706
|
+
{
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let tmpDuration = tmpAEX._waveformData.Duration || 0;
|
|
711
|
+
if (tmpDuration <= 0)
|
|
712
|
+
{
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Convert normalized selection (0..1) to seconds
|
|
717
|
+
let tmpStart = 0;
|
|
718
|
+
let tmpEnd = tmpDuration;
|
|
719
|
+
if (tmpAEX._selectionStart >= 0 && tmpAEX._selectionEnd >= 0)
|
|
720
|
+
{
|
|
721
|
+
tmpStart = Math.round(tmpAEX._selectionStart * tmpDuration * 100) / 100;
|
|
722
|
+
tmpEnd = Math.round(tmpAEX._selectionEnd * tmpDuration * 100) / 100;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
let tmpItem =
|
|
726
|
+
{
|
|
727
|
+
Type: 'video-clip', // reuse video-clip type for audio time ranges
|
|
728
|
+
Path: tmpAEX._currentPath,
|
|
729
|
+
VideoStart: tmpStart,
|
|
730
|
+
VideoEnd: tmpEnd,
|
|
731
|
+
Label: this._formatTimestamp(tmpStart) + ' - ' + this._formatTimestamp(tmpEnd),
|
|
732
|
+
Note: ''
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
this.addItemsToCollection(tmpTargetGUID, [tmpItem]);
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Format a timestamp in seconds to a human-readable string.
|
|
741
|
+
*
|
|
742
|
+
* @param {number} pSeconds - Timestamp in seconds
|
|
743
|
+
* @returns {string} Formatted string like "1:23" or "1:01:23"
|
|
744
|
+
*/
|
|
745
|
+
_formatTimestamp(pSeconds)
|
|
746
|
+
{
|
|
747
|
+
if (typeof pSeconds !== 'number' || isNaN(pSeconds))
|
|
748
|
+
{
|
|
749
|
+
return '0:00';
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
let tmpTotalSeconds = Math.floor(pSeconds);
|
|
753
|
+
let tmpHours = Math.floor(tmpTotalSeconds / 3600);
|
|
754
|
+
let tmpMinutes = Math.floor((tmpTotalSeconds % 3600) / 60);
|
|
755
|
+
let tmpSecs = tmpTotalSeconds % 60;
|
|
756
|
+
|
|
757
|
+
if (tmpHours > 0)
|
|
758
|
+
{
|
|
759
|
+
return tmpHours + ':' + (tmpMinutes < 10 ? '0' : '') + tmpMinutes + ':' + (tmpSecs < 10 ? '0' : '') + tmpSecs;
|
|
760
|
+
}
|
|
761
|
+
return tmpMinutes + ':' + (tmpSecs < 10 ? '0' : '') + tmpSecs;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Add the currently browsed folder to a collection.
|
|
766
|
+
*
|
|
767
|
+
* @param {string} pGUID - Collection GUID
|
|
768
|
+
* @param {string} pMode - "folder" (add folder reference) or "contents" (add folder contents wildcard)
|
|
769
|
+
*/
|
|
770
|
+
addCurrentFolderToCollection(pGUID, pMode)
|
|
771
|
+
{
|
|
772
|
+
let tmpRemote = this._getRemote();
|
|
773
|
+
let tmpCurrentPath = (this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation) || '';
|
|
774
|
+
|
|
775
|
+
if (!tmpCurrentPath || !pGUID)
|
|
776
|
+
{
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
let tmpItem =
|
|
781
|
+
{
|
|
782
|
+
Type: (pMode === 'contents') ? 'folder-contents' : 'folder',
|
|
783
|
+
Path: tmpCurrentPath,
|
|
784
|
+
Label: '',
|
|
785
|
+
Note: ''
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
this.addItemsToCollection(pGUID, [tmpItem]);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Sort a collection's Items array in place (client-side mirror of server logic).
|
|
793
|
+
*
|
|
794
|
+
* @param {Array} pItems - Items array
|
|
795
|
+
* @param {string} pSortMode - "manual" | "name" | "modified" | "type"
|
|
796
|
+
* @param {string} pSortDirection - "asc" | "desc"
|
|
797
|
+
* @returns {Array} The same array, sorted
|
|
798
|
+
*/
|
|
799
|
+
_sortItems(pItems, pSortMode, pSortDirection)
|
|
800
|
+
{
|
|
801
|
+
if (!Array.isArray(pItems) || pItems.length < 2)
|
|
802
|
+
{
|
|
803
|
+
return pItems;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
let tmpDirection = (pSortDirection === 'desc') ? -1 : 1;
|
|
807
|
+
|
|
808
|
+
switch (pSortMode)
|
|
809
|
+
{
|
|
810
|
+
case 'name':
|
|
811
|
+
pItems.sort((a, b) =>
|
|
812
|
+
{
|
|
813
|
+
let tmpA = (a.Label || a.Path || '').toLowerCase();
|
|
814
|
+
let tmpB = (b.Label || b.Path || '').toLowerCase();
|
|
815
|
+
// Sort by filename portion only (after last /)
|
|
816
|
+
let tmpSlashA = tmpA.lastIndexOf('/');
|
|
817
|
+
let tmpSlashB = tmpB.lastIndexOf('/');
|
|
818
|
+
if (tmpSlashA >= 0) tmpA = tmpA.substring(tmpSlashA + 1);
|
|
819
|
+
if (tmpSlashB >= 0) tmpB = tmpB.substring(tmpSlashB + 1);
|
|
820
|
+
return tmpDirection * tmpA.localeCompare(tmpB);
|
|
821
|
+
});
|
|
822
|
+
break;
|
|
823
|
+
|
|
824
|
+
case 'type':
|
|
825
|
+
pItems.sort((a, b) =>
|
|
826
|
+
{
|
|
827
|
+
let tmpA = (a.Type || '').toLowerCase();
|
|
828
|
+
let tmpB = (b.Type || '').toLowerCase();
|
|
829
|
+
return tmpDirection * tmpA.localeCompare(tmpB);
|
|
830
|
+
});
|
|
831
|
+
break;
|
|
832
|
+
|
|
833
|
+
case 'modified':
|
|
834
|
+
pItems.sort((a, b) =>
|
|
835
|
+
{
|
|
836
|
+
let tmpA = a.AddedAt || '';
|
|
837
|
+
let tmpB = b.AddedAt || '';
|
|
838
|
+
return tmpDirection * tmpA.localeCompare(tmpB);
|
|
839
|
+
});
|
|
840
|
+
break;
|
|
841
|
+
|
|
842
|
+
case 'manual':
|
|
843
|
+
default:
|
|
844
|
+
pItems.sort((a, b) =>
|
|
845
|
+
{
|
|
846
|
+
return tmpDirection * ((a.SortOrder || 0) - (b.SortOrder || 0));
|
|
847
|
+
});
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return pItems;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Sort the active collection's items in place and re-render the panel.
|
|
856
|
+
* Saves the sort preference to the server in the background.
|
|
857
|
+
*
|
|
858
|
+
* @param {string} pSortMode - Sort mode (or null to keep current)
|
|
859
|
+
* @param {string} pSortDirection - Sort direction (or null to keep current)
|
|
860
|
+
*/
|
|
861
|
+
sortActiveCollection(pSortMode, pSortDirection)
|
|
862
|
+
{
|
|
863
|
+
let tmpRemote = this._getRemote();
|
|
864
|
+
let tmpCollection = tmpRemote.ActiveCollection;
|
|
865
|
+
|
|
866
|
+
if (!tmpCollection)
|
|
867
|
+
{
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Update sort preferences
|
|
872
|
+
if (typeof pSortMode === 'string')
|
|
873
|
+
{
|
|
874
|
+
tmpCollection.SortMode = pSortMode;
|
|
875
|
+
}
|
|
876
|
+
if (typeof pSortDirection === 'string')
|
|
877
|
+
{
|
|
878
|
+
tmpCollection.SortDirection = pSortDirection;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Sort items locally
|
|
882
|
+
this._sortItems(tmpCollection.Items || [], tmpCollection.SortMode, tmpCollection.SortDirection);
|
|
883
|
+
|
|
884
|
+
// Re-render the panel immediately
|
|
885
|
+
let tmpPanel = this._getPanelView();
|
|
886
|
+
if (tmpPanel)
|
|
887
|
+
{
|
|
888
|
+
tmpPanel.renderContent();
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Save sort preference to server in background (no re-render needed)
|
|
892
|
+
fetch('/api/collections/' + encodeURIComponent(tmpCollection.GUID),
|
|
893
|
+
{
|
|
894
|
+
method: 'PUT',
|
|
895
|
+
headers: { 'Content-Type': 'application/json' },
|
|
896
|
+
body: JSON.stringify({ GUID: tmpCollection.GUID, SortMode: tmpCollection.SortMode, SortDirection: tmpCollection.SortDirection })
|
|
897
|
+
})
|
|
898
|
+
.catch((pError) =>
|
|
899
|
+
{
|
|
900
|
+
this.log.error('Failed to save sort preference: ' + pError.message);
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Search the loaded collections by name/description/tags (client-side filter).
|
|
906
|
+
*
|
|
907
|
+
* @param {string} pQuery - Search query
|
|
908
|
+
* @returns {Array} Filtered collection summaries
|
|
909
|
+
*/
|
|
910
|
+
searchCollections(pQuery)
|
|
911
|
+
{
|
|
912
|
+
let tmpRemote = this._getRemote();
|
|
913
|
+
let tmpQuery = (pQuery || '').toLowerCase();
|
|
914
|
+
|
|
915
|
+
if (!tmpQuery)
|
|
916
|
+
{
|
|
917
|
+
return tmpRemote.Collections;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return tmpRemote.Collections.filter((pCollection) =>
|
|
921
|
+
{
|
|
922
|
+
let tmpName = (pCollection.Name || '').toLowerCase();
|
|
923
|
+
let tmpDesc = (pCollection.Description || '').toLowerCase();
|
|
924
|
+
let tmpTags = (pCollection.Tags || []).join(' ').toLowerCase();
|
|
925
|
+
return tmpName.indexOf(tmpQuery) >= 0 ||
|
|
926
|
+
tmpDesc.indexOf(tmpQuery) >= 0 ||
|
|
927
|
+
tmpTags.indexOf(tmpQuery) >= 0;
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
CollectionManagerProvider.default_configuration = _DefaultProviderConfiguration;
|
|
933
|
+
|
|
934
|
+
module.exports = CollectionManagerProvider;
|