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.
Files changed (63) hide show
  1. package/docs/README.md +181 -0
  2. package/docs/_cover.md +14 -0
  3. package/docs/_sidebar.md +10 -0
  4. package/docs/_topbar.md +3 -0
  5. package/docs/audio-viewer.md +133 -0
  6. package/docs/ebook-reader.md +90 -0
  7. package/docs/image-viewer.md +90 -0
  8. package/docs/server-setup.md +262 -0
  9. package/docs/video-viewer.md +134 -0
  10. package/html/docs.html +59 -0
  11. package/package.json +21 -7
  12. package/source/Pict-Application-RetoldRemote.js +143 -2
  13. package/source/RetoldRemote-ExtensionMaps.js +33 -0
  14. package/source/cli/RetoldRemote-Server-Setup.js +82 -67
  15. package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
  16. package/source/providers/Pict-Provider-CollectionManager.js +934 -0
  17. package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
  18. package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
  19. package/source/providers/Pict-Provider-GalleryNavigation.js +270 -353
  20. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
  21. package/source/providers/Pict-Provider-ToastNotification.js +96 -0
  22. package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
  23. package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
  24. package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
  25. package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
  26. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
  27. package/source/server/RetoldRemote-ArchiveService.js +2 -12
  28. package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
  29. package/source/server/RetoldRemote-CollectionService.js +684 -0
  30. package/source/server/RetoldRemote-EbookService.js +7 -16
  31. package/source/server/RetoldRemote-MediaService.js +3 -14
  32. package/source/server/RetoldRemote-ParimeCache.js +349 -0
  33. package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
  34. package/source/server/RetoldRemote-VideoFrameService.js +7 -15
  35. package/source/views/PictView-Remote-AudioExplorer.js +10 -43
  36. package/source/views/PictView-Remote-CollectionsPanel.js +1087 -0
  37. package/source/views/PictView-Remote-Gallery.js +237 -44
  38. package/source/views/PictView-Remote-ImageViewer.js +1 -34
  39. package/source/views/PictView-Remote-Layout.js +410 -20
  40. package/source/views/PictView-Remote-MediaViewer.js +338 -51
  41. package/source/views/PictView-Remote-SettingsPanel.js +155 -138
  42. package/source/views/PictView-Remote-TopBar.js +615 -14
  43. package/source/views/PictView-Remote-VLCSetup.js +766 -0
  44. package/source/views/PictView-Remote-VideoExplorer.js +20 -54
  45. package/web-application/css/docuserve.css +73 -0
  46. package/web-application/docs/README.md +181 -0
  47. package/web-application/docs/_cover.md +14 -0
  48. package/web-application/docs/_sidebar.md +10 -0
  49. package/web-application/docs/_topbar.md +3 -0
  50. package/web-application/docs/audio-viewer.md +133 -0
  51. package/web-application/docs/ebook-reader.md +90 -0
  52. package/web-application/docs/image-viewer.md +90 -0
  53. package/web-application/docs/server-setup.md +262 -0
  54. package/web-application/docs/video-viewer.md +134 -0
  55. package/web-application/docs.html +59 -0
  56. package/web-application/js/pict-docuserve.min.js +58 -0
  57. package/web-application/js/pict.min.js +2 -2
  58. package/web-application/js/pict.min.js.map +1 -1
  59. package/web-application/retold-remote.js +2558 -439
  60. package/web-application/retold-remote.js.map +1 -1
  61. package/web-application/retold-remote.min.js +41 -11
  62. package/web-application/retold-remote.min.js.map +1 -1
  63. package/server.js +0 -43
@@ -0,0 +1,1087 @@
1
+ /**
2
+ * Retold Remote -- Collections Panel View
3
+ *
4
+ * Right-side flyout panel for managing user-defined collections.
5
+ * Mirrors the left sidebar architecture: flex child inside
6
+ * .content-editor-body, with collapse/expand, resize handle,
7
+ * and mobile responsive bottom-drawer mode.
8
+ *
9
+ * Three modes:
10
+ * list — browseable list of all collections
11
+ * detail — items inside a selected collection
12
+ * edit — metadata editing for a collection
13
+ *
14
+ * @license MIT
15
+ */
16
+ const libPictView = require('pict-view');
17
+
18
+ const _ViewConfiguration =
19
+ {
20
+ ViewIdentifier: "RetoldRemote-CollectionsPanel",
21
+ DefaultRenderable: "RetoldRemote-CollectionsPanel",
22
+ DefaultDestinationAddress: "#RetoldRemote-Collections-Container",
23
+ AutoRender: false,
24
+
25
+ CSS: /*css*/`
26
+ /* ---- Collections Panel Container ---- */
27
+ .retold-remote-collections-panel
28
+ {
29
+ display: flex;
30
+ flex-direction: column;
31
+ height: 100%;
32
+ overflow: hidden;
33
+ }
34
+ .retold-remote-collections-header
35
+ {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 8px;
39
+ padding: 8px 12px;
40
+ border-bottom: 1px solid var(--retold-border);
41
+ background: var(--retold-bg-secondary);
42
+ flex-shrink: 0;
43
+ }
44
+ .retold-remote-collections-header-title
45
+ {
46
+ flex: 1;
47
+ font-size: 0.82rem;
48
+ font-weight: 600;
49
+ color: var(--retold-text-primary);
50
+ white-space: nowrap;
51
+ overflow: hidden;
52
+ text-overflow: ellipsis;
53
+ }
54
+ .retold-remote-collections-header-btn
55
+ {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ width: 26px;
60
+ height: 26px;
61
+ padding: 0;
62
+ border: 1px solid var(--retold-border);
63
+ border-radius: 4px;
64
+ background: transparent;
65
+ color: var(--retold-text-muted);
66
+ font-size: 0.82rem;
67
+ cursor: pointer;
68
+ transition: color 0.15s, border-color 0.15s;
69
+ font-family: inherit;
70
+ flex-shrink: 0;
71
+ }
72
+ .retold-remote-collections-header-btn:hover
73
+ {
74
+ color: var(--retold-text-primary);
75
+ border-color: var(--retold-accent);
76
+ }
77
+ /* ---- Search ---- */
78
+ .retold-remote-collections-search
79
+ {
80
+ padding: 6px 12px;
81
+ border-bottom: 1px solid var(--retold-border);
82
+ flex-shrink: 0;
83
+ }
84
+ .retold-remote-collections-search input
85
+ {
86
+ width: 100%;
87
+ padding: 5px 8px;
88
+ border: 1px solid var(--retold-border);
89
+ border-radius: 3px;
90
+ background: var(--retold-bg-tertiary);
91
+ color: var(--retold-text-secondary);
92
+ font-size: 0.78rem;
93
+ font-family: inherit;
94
+ box-sizing: border-box;
95
+ outline: none;
96
+ }
97
+ .retold-remote-collections-search input:focus
98
+ {
99
+ border-color: var(--retold-accent);
100
+ }
101
+ /* ---- Collection List ---- */
102
+ .retold-remote-collections-body
103
+ {
104
+ flex: 1;
105
+ overflow-y: auto;
106
+ overflow-x: hidden;
107
+ }
108
+ .retold-remote-collections-empty
109
+ {
110
+ padding: 24px 16px;
111
+ text-align: center;
112
+ font-size: 0.78rem;
113
+ color: var(--retold-text-dim);
114
+ }
115
+ .retold-remote-collection-card
116
+ {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 10px;
120
+ padding: 10px 12px;
121
+ border-bottom: 1px solid var(--retold-border);
122
+ cursor: pointer;
123
+ transition: background 0.15s;
124
+ }
125
+ .retold-remote-collection-card:hover
126
+ {
127
+ background: rgba(128, 128, 128, 0.08);
128
+ }
129
+ .retold-remote-collection-card-icon
130
+ {
131
+ flex-shrink: 0;
132
+ width: 32px;
133
+ height: 32px;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ border-radius: 4px;
138
+ background: var(--retold-bg-tertiary);
139
+ color: var(--retold-accent);
140
+ font-size: 1rem;
141
+ }
142
+ .retold-remote-collection-card-info
143
+ {
144
+ flex: 1;
145
+ min-width: 0;
146
+ }
147
+ .retold-remote-collection-card-name
148
+ {
149
+ font-size: 0.82rem;
150
+ font-weight: 500;
151
+ color: var(--retold-text-primary);
152
+ white-space: nowrap;
153
+ overflow: hidden;
154
+ text-overflow: ellipsis;
155
+ }
156
+ .retold-remote-collection-card-meta
157
+ {
158
+ font-size: 0.68rem;
159
+ color: var(--retold-text-dim);
160
+ margin-top: 2px;
161
+ }
162
+ /* ---- Detail mode ---- */
163
+ .retold-remote-collections-detail-controls
164
+ {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 6px;
168
+ padding: 6px 12px;
169
+ border-bottom: 1px solid var(--retold-border);
170
+ flex-shrink: 0;
171
+ }
172
+ .retold-remote-collections-sort-select
173
+ {
174
+ padding: 3px 6px;
175
+ border: 1px solid var(--retold-border);
176
+ border-radius: 3px;
177
+ background: var(--retold-bg-tertiary);
178
+ color: var(--retold-text-secondary);
179
+ font-size: 0.72rem;
180
+ font-family: inherit;
181
+ flex: 1;
182
+ }
183
+ .retold-remote-collections-sort-dir
184
+ {
185
+ padding: 3px 6px;
186
+ border: 1px solid var(--retold-border);
187
+ border-radius: 3px;
188
+ background: transparent;
189
+ color: var(--retold-text-muted);
190
+ font-size: 0.72rem;
191
+ cursor: pointer;
192
+ font-family: inherit;
193
+ }
194
+ .retold-remote-collections-sort-dir:hover
195
+ {
196
+ border-color: var(--retold-accent);
197
+ color: var(--retold-text-primary);
198
+ }
199
+ /* ---- Item rows ---- */
200
+ .retold-remote-collection-item
201
+ {
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 8px;
205
+ padding: 6px 12px;
206
+ border-bottom: 1px solid var(--retold-border);
207
+ cursor: pointer;
208
+ transition: background 0.15s;
209
+ position: relative;
210
+ }
211
+ .retold-remote-collection-item:hover
212
+ {
213
+ background: rgba(128, 128, 128, 0.08);
214
+ }
215
+ .retold-remote-collection-item-drag
216
+ {
217
+ flex-shrink: 0;
218
+ width: 16px;
219
+ cursor: grab;
220
+ color: var(--retold-text-dim);
221
+ font-size: 0.68rem;
222
+ text-align: center;
223
+ user-select: none;
224
+ }
225
+ .retold-remote-collection-item-icon
226
+ {
227
+ flex-shrink: 0;
228
+ width: 24px;
229
+ height: 24px;
230
+ display: flex;
231
+ align-items: center;
232
+ justify-content: center;
233
+ color: var(--retold-text-muted);
234
+ font-size: 0.82rem;
235
+ }
236
+ .retold-remote-collection-item-icon img
237
+ {
238
+ width: 24px;
239
+ height: 24px;
240
+ object-fit: cover;
241
+ border-radius: 2px;
242
+ }
243
+ .retold-remote-collection-item-name
244
+ {
245
+ flex: 1;
246
+ min-width: 0;
247
+ font-size: 0.78rem;
248
+ color: var(--retold-text-secondary);
249
+ white-space: nowrap;
250
+ overflow: hidden;
251
+ text-overflow: ellipsis;
252
+ }
253
+ .retold-remote-collection-item-type
254
+ {
255
+ flex-shrink: 0;
256
+ font-size: 0.62rem;
257
+ padding: 1px 4px;
258
+ border-radius: 2px;
259
+ background: var(--retold-bg-tertiary);
260
+ color: var(--retold-text-dim);
261
+ text-transform: uppercase;
262
+ }
263
+ .retold-remote-collection-item-remove
264
+ {
265
+ flex-shrink: 0;
266
+ display: none;
267
+ width: 20px;
268
+ height: 20px;
269
+ align-items: center;
270
+ justify-content: center;
271
+ border: none;
272
+ border-radius: 3px;
273
+ background: transparent;
274
+ color: var(--retold-text-dim);
275
+ font-size: 0.82rem;
276
+ cursor: pointer;
277
+ padding: 0;
278
+ }
279
+ .retold-remote-collection-item:hover .retold-remote-collection-item-remove
280
+ {
281
+ display: inline-flex;
282
+ }
283
+ .retold-remote-collection-item-remove:hover
284
+ {
285
+ color: var(--retold-danger-muted, #e55);
286
+ background: rgba(200, 50, 50, 0.1);
287
+ }
288
+ /* ---- Edit mode ---- */
289
+ .retold-remote-collections-edit
290
+ {
291
+ padding: 12px;
292
+ }
293
+ .retold-remote-collections-edit-group
294
+ {
295
+ margin-bottom: 12px;
296
+ }
297
+ .retold-remote-collections-edit-label
298
+ {
299
+ font-size: 0.7rem;
300
+ font-weight: 600;
301
+ text-transform: uppercase;
302
+ letter-spacing: 0.5px;
303
+ color: var(--retold-text-dim);
304
+ margin-bottom: 4px;
305
+ }
306
+ .retold-remote-collections-edit-input
307
+ {
308
+ width: 100%;
309
+ padding: 6px 8px;
310
+ border: 1px solid var(--retold-border);
311
+ border-radius: 3px;
312
+ background: var(--retold-bg-tertiary);
313
+ color: var(--retold-text-secondary);
314
+ font-size: 0.82rem;
315
+ font-family: inherit;
316
+ box-sizing: border-box;
317
+ }
318
+ .retold-remote-collections-edit-input:focus
319
+ {
320
+ border-color: var(--retold-accent);
321
+ outline: none;
322
+ }
323
+ .retold-remote-collections-edit-textarea
324
+ {
325
+ width: 100%;
326
+ min-height: 80px;
327
+ padding: 6px 8px;
328
+ border: 1px solid var(--retold-border);
329
+ border-radius: 3px;
330
+ background: var(--retold-bg-tertiary);
331
+ color: var(--retold-text-secondary);
332
+ font-size: 0.78rem;
333
+ font-family: inherit;
334
+ box-sizing: border-box;
335
+ resize: vertical;
336
+ }
337
+ .retold-remote-collections-edit-textarea:focus
338
+ {
339
+ border-color: var(--retold-accent);
340
+ outline: none;
341
+ }
342
+ .retold-remote-collections-edit-actions
343
+ {
344
+ display: flex;
345
+ gap: 8px;
346
+ margin-top: 16px;
347
+ }
348
+ .retold-remote-collections-edit-btn
349
+ {
350
+ padding: 6px 14px;
351
+ border: 1px solid var(--retold-border);
352
+ border-radius: 4px;
353
+ background: transparent;
354
+ color: var(--retold-text-secondary);
355
+ font-size: 0.78rem;
356
+ cursor: pointer;
357
+ font-family: inherit;
358
+ }
359
+ .retold-remote-collections-edit-btn:hover
360
+ {
361
+ border-color: var(--retold-accent);
362
+ color: var(--retold-text-primary);
363
+ }
364
+ .retold-remote-collections-edit-btn-primary
365
+ {
366
+ background: var(--retold-accent);
367
+ border-color: var(--retold-accent);
368
+ color: #fff;
369
+ }
370
+ .retold-remote-collections-edit-btn-primary:hover
371
+ {
372
+ opacity: 0.9;
373
+ }
374
+ .retold-remote-collections-edit-btn-danger
375
+ {
376
+ border-color: var(--retold-danger-muted, #e55);
377
+ color: var(--retold-danger-muted, #e55);
378
+ margin-top: 16px;
379
+ }
380
+ .retold-remote-collections-edit-btn-danger:hover
381
+ {
382
+ background: rgba(200, 50, 50, 0.1);
383
+ }
384
+ /* ---- Drag-and-drop feedback ---- */
385
+ .retold-remote-collection-item.dragging
386
+ {
387
+ opacity: 0.4;
388
+ }
389
+ .retold-remote-collection-item.drag-over
390
+ {
391
+ border-top: 2px solid var(--retold-accent);
392
+ }
393
+ `,
394
+
395
+ Templates:
396
+ [
397
+ {
398
+ Hash: "RetoldRemote-CollectionsPanel",
399
+ Template: /*html*/`<div class="retold-remote-collections-panel" id="RetoldRemote-CollectionsPanel-Root"></div>`
400
+ }
401
+ ],
402
+
403
+ Renderables:
404
+ [
405
+ {
406
+ RenderableHash: "RetoldRemote-CollectionsPanel",
407
+ TemplateHash: "RetoldRemote-CollectionsPanel",
408
+ DestinationAddress: "#RetoldRemote-Collections-Container"
409
+ }
410
+ ]
411
+ };
412
+
413
+ class RetoldRemoteCollectionsPanelView extends libPictView
414
+ {
415
+ constructor(pFable, pOptions, pServiceHash)
416
+ {
417
+ super(pFable, pOptions, pServiceHash);
418
+
419
+ this._draggedItemId = null;
420
+ }
421
+
422
+ onAfterRender()
423
+ {
424
+ super.onAfterRender();
425
+ this.renderContent();
426
+ }
427
+
428
+ /**
429
+ * Main render dispatch — renders the appropriate mode content
430
+ * into the panel root.
431
+ */
432
+ renderContent()
433
+ {
434
+ let tmpRoot = document.getElementById('RetoldRemote-CollectionsPanel-Root');
435
+ if (!tmpRoot)
436
+ {
437
+ return;
438
+ }
439
+
440
+ let tmpRemote = this.pict.AppData.RetoldRemote;
441
+
442
+ switch (tmpRemote.CollectionsPanelMode)
443
+ {
444
+ case 'detail':
445
+ this._renderDetailMode(tmpRoot);
446
+ break;
447
+ case 'edit':
448
+ this._renderEditMode(tmpRoot);
449
+ break;
450
+ case 'list':
451
+ default:
452
+ this._renderListMode(tmpRoot);
453
+ break;
454
+ }
455
+ }
456
+
457
+ // -- List Mode --------------------------------------------------------
458
+
459
+ _renderListMode(pRoot)
460
+ {
461
+ let tmpSelf = this;
462
+ let tmpRemote = this.pict.AppData.RetoldRemote;
463
+ let tmpManager = this.pict.providers['RetoldRemote-CollectionManager'];
464
+
465
+ pRoot.innerHTML = '';
466
+
467
+ // Header
468
+ let tmpHeader = document.createElement('div');
469
+ tmpHeader.className = 'retold-remote-collections-header';
470
+
471
+ let tmpTitle = document.createElement('div');
472
+ tmpTitle.className = 'retold-remote-collections-header-title';
473
+ tmpTitle.textContent = 'Collections';
474
+
475
+ let tmpNewBtn = document.createElement('button');
476
+ tmpNewBtn.className = 'retold-remote-collections-header-btn';
477
+ tmpNewBtn.title = 'New Collection';
478
+ tmpNewBtn.textContent = '+';
479
+ tmpNewBtn.onclick = () =>
480
+ {
481
+ let tmpName = prompt('Collection name:');
482
+ if (tmpName && tmpName.trim())
483
+ {
484
+ tmpManager.createCollection(tmpName.trim(), (pError, pCollection) =>
485
+ {
486
+ if (!pError && pCollection)
487
+ {
488
+ // Open the newly created collection
489
+ tmpRemote.CollectionsPanelMode = 'detail';
490
+ tmpManager.fetchCollection(pCollection.GUID);
491
+ }
492
+ });
493
+ }
494
+ };
495
+
496
+ tmpHeader.appendChild(tmpTitle);
497
+ tmpHeader.appendChild(tmpNewBtn);
498
+ pRoot.appendChild(tmpHeader);
499
+
500
+ // Search
501
+ let tmpSearchWrap = document.createElement('div');
502
+ tmpSearchWrap.className = 'retold-remote-collections-search';
503
+
504
+ let tmpSearchInput = document.createElement('input');
505
+ tmpSearchInput.type = 'text';
506
+ tmpSearchInput.placeholder = 'Search collections...';
507
+ tmpSearchInput.value = tmpRemote.CollectionSearchQuery || '';
508
+ tmpSearchInput.oninput = (pEvent) =>
509
+ {
510
+ tmpRemote.CollectionSearchQuery = pEvent.target.value;
511
+ tmpSelf._renderCollectionCards(pRoot.querySelector('.retold-remote-collections-body'));
512
+ };
513
+
514
+ tmpSearchWrap.appendChild(tmpSearchInput);
515
+ pRoot.appendChild(tmpSearchWrap);
516
+
517
+ // Body (scrollable list)
518
+ let tmpBody = document.createElement('div');
519
+ tmpBody.className = 'retold-remote-collections-body';
520
+ pRoot.appendChild(tmpBody);
521
+
522
+ this._renderCollectionCards(tmpBody);
523
+ }
524
+
525
+ _renderCollectionCards(pBody)
526
+ {
527
+ if (!pBody)
528
+ {
529
+ return;
530
+ }
531
+
532
+ let tmpSelf = this;
533
+ let tmpRemote = this.pict.AppData.RetoldRemote;
534
+ let tmpManager = this.pict.providers['RetoldRemote-CollectionManager'];
535
+ let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
536
+
537
+ pBody.innerHTML = '';
538
+
539
+ let tmpCollections = tmpManager.searchCollections(tmpRemote.CollectionSearchQuery);
540
+
541
+ if (!tmpCollections || tmpCollections.length === 0)
542
+ {
543
+ let tmpEmpty = document.createElement('div');
544
+ tmpEmpty.className = 'retold-remote-collections-empty';
545
+ tmpEmpty.textContent = tmpRemote.CollectionSearchQuery ? 'No collections match your search.' : 'No collections yet. Click + to create one.';
546
+ pBody.appendChild(tmpEmpty);
547
+ return;
548
+ }
549
+
550
+ for (let i = 0; i < tmpCollections.length; i++)
551
+ {
552
+ let tmpCollection = tmpCollections[i];
553
+
554
+ let tmpCard = document.createElement('div');
555
+ tmpCard.className = 'retold-remote-collection-card';
556
+ tmpCard.onclick = () =>
557
+ {
558
+ tmpRemote.CollectionsPanelMode = 'detail';
559
+ tmpManager.fetchCollection(tmpCollection.GUID);
560
+ };
561
+
562
+ // Icon
563
+ let tmpIconDiv = document.createElement('div');
564
+ tmpIconDiv.className = 'retold-remote-collection-card-icon';
565
+ if (tmpIconProvider && typeof tmpIconProvider.getIcon === 'function')
566
+ {
567
+ tmpIconDiv.innerHTML = tmpIconProvider.getIcon('bookmark', 18);
568
+ }
569
+ else
570
+ {
571
+ tmpIconDiv.textContent = '\u2630';
572
+ }
573
+
574
+ // Info
575
+ let tmpInfoDiv = document.createElement('div');
576
+ tmpInfoDiv.className = 'retold-remote-collection-card-info';
577
+
578
+ let tmpNameDiv = document.createElement('div');
579
+ tmpNameDiv.className = 'retold-remote-collection-card-name';
580
+ tmpNameDiv.textContent = tmpCollection.Name || 'Untitled';
581
+
582
+ let tmpMetaDiv = document.createElement('div');
583
+ tmpMetaDiv.className = 'retold-remote-collection-card-meta';
584
+ tmpMetaDiv.textContent = tmpCollection.ItemCount + ' item' + (tmpCollection.ItemCount !== 1 ? 's' : '');
585
+
586
+ tmpInfoDiv.appendChild(tmpNameDiv);
587
+ tmpInfoDiv.appendChild(tmpMetaDiv);
588
+
589
+ tmpCard.appendChild(tmpIconDiv);
590
+ tmpCard.appendChild(tmpInfoDiv);
591
+ pBody.appendChild(tmpCard);
592
+ }
593
+ }
594
+
595
+ // -- Detail Mode ------------------------------------------------------
596
+
597
+ _renderDetailMode(pRoot)
598
+ {
599
+ let tmpSelf = this;
600
+ let tmpRemote = this.pict.AppData.RetoldRemote;
601
+ let tmpManager = this.pict.providers['RetoldRemote-CollectionManager'];
602
+ let tmpCollection = tmpRemote.ActiveCollection;
603
+
604
+ pRoot.innerHTML = '';
605
+
606
+ if (!tmpCollection)
607
+ {
608
+ pRoot.innerHTML = '<div class="retold-remote-collections-empty">Loading...</div>';
609
+ return;
610
+ }
611
+
612
+ // Header with back button and collection name
613
+ let tmpHeader = document.createElement('div');
614
+ tmpHeader.className = 'retold-remote-collections-header';
615
+
616
+ let tmpBackBtn = document.createElement('button');
617
+ tmpBackBtn.className = 'retold-remote-collections-header-btn';
618
+ tmpBackBtn.title = 'Back to list';
619
+ tmpBackBtn.textContent = '\u2190';
620
+ tmpBackBtn.onclick = () =>
621
+ {
622
+ tmpRemote.CollectionsPanelMode = 'list';
623
+ tmpRemote.ActiveCollectionGUID = null;
624
+ tmpRemote.ActiveCollection = null;
625
+ tmpSelf.renderContent();
626
+ };
627
+
628
+ let tmpTitle = document.createElement('div');
629
+ tmpTitle.className = 'retold-remote-collections-header-title';
630
+ tmpTitle.textContent = tmpCollection.Name || 'Untitled';
631
+
632
+ let tmpEditBtn = document.createElement('button');
633
+ tmpEditBtn.className = 'retold-remote-collections-header-btn';
634
+ tmpEditBtn.title = 'Edit collection';
635
+ tmpEditBtn.textContent = '\u270E';
636
+ tmpEditBtn.onclick = () =>
637
+ {
638
+ tmpRemote.CollectionsPanelMode = 'edit';
639
+ tmpSelf.renderContent();
640
+ };
641
+
642
+ tmpHeader.appendChild(tmpBackBtn);
643
+ tmpHeader.appendChild(tmpTitle);
644
+ tmpHeader.appendChild(tmpEditBtn);
645
+ pRoot.appendChild(tmpHeader);
646
+
647
+ // Sort controls
648
+ let tmpControls = document.createElement('div');
649
+ tmpControls.className = 'retold-remote-collections-detail-controls';
650
+
651
+ let tmpSortSelect = document.createElement('select');
652
+ tmpSortSelect.className = 'retold-remote-collections-sort-select';
653
+
654
+ let tmpSortOptions = [
655
+ { value: 'manual', label: 'Manual' },
656
+ { value: 'name', label: 'Name' },
657
+ { value: 'modified', label: 'Date Added' },
658
+ { value: 'type', label: 'Type' }
659
+ ];
660
+
661
+ for (let i = 0; i < tmpSortOptions.length; i++)
662
+ {
663
+ let tmpOpt = document.createElement('option');
664
+ tmpOpt.value = tmpSortOptions[i].value;
665
+ tmpOpt.textContent = tmpSortOptions[i].label;
666
+ if (tmpCollection.SortMode === tmpSortOptions[i].value)
667
+ {
668
+ tmpOpt.selected = true;
669
+ }
670
+ tmpSortSelect.appendChild(tmpOpt);
671
+ }
672
+
673
+ tmpSortSelect.onchange = (pEvent) =>
674
+ {
675
+ tmpManager.sortActiveCollection(pEvent.target.value, null);
676
+ };
677
+
678
+ let tmpDirBtn = document.createElement('button');
679
+ tmpDirBtn.className = 'retold-remote-collections-sort-dir';
680
+ tmpDirBtn.textContent = tmpCollection.SortDirection === 'desc' ? '\u2193' : '\u2191';
681
+ tmpDirBtn.title = tmpCollection.SortDirection === 'desc' ? 'Descending' : 'Ascending';
682
+ tmpDirBtn.onclick = () =>
683
+ {
684
+ let tmpNewDir = (tmpRemote.ActiveCollection.SortDirection === 'desc') ? 'asc' : 'desc';
685
+ tmpManager.sortActiveCollection(null, tmpNewDir);
686
+ };
687
+
688
+ tmpControls.appendChild(tmpSortSelect);
689
+ tmpControls.appendChild(tmpDirBtn);
690
+ pRoot.appendChild(tmpControls);
691
+
692
+ // Item list
693
+ let tmpBody = document.createElement('div');
694
+ tmpBody.className = 'retold-remote-collections-body';
695
+ pRoot.appendChild(tmpBody);
696
+
697
+ this._renderItemList(tmpBody, tmpCollection);
698
+ }
699
+
700
+ _renderItemList(pBody, pCollection)
701
+ {
702
+ let tmpSelf = this;
703
+ let tmpRemote = this.pict.AppData.RetoldRemote;
704
+ let tmpManager = this.pict.providers['RetoldRemote-CollectionManager'];
705
+ let tmpItems = pCollection.Items || [];
706
+
707
+ pBody.innerHTML = '';
708
+
709
+ if (tmpItems.length === 0)
710
+ {
711
+ let tmpEmpty = document.createElement('div');
712
+ tmpEmpty.className = 'retold-remote-collections-empty';
713
+ tmpEmpty.textContent = 'No items yet. Browse files and add them to this collection.';
714
+ pBody.appendChild(tmpEmpty);
715
+ return;
716
+ }
717
+
718
+ for (let i = 0; i < tmpItems.length; i++)
719
+ {
720
+ let tmpItem = tmpItems[i];
721
+ let tmpRow = document.createElement('div');
722
+ tmpRow.className = 'retold-remote-collection-item';
723
+ tmpRow.setAttribute('data-item-id', tmpItem.ID);
724
+
725
+ // Drag handle (for manual sort)
726
+ if (pCollection.SortMode === 'manual')
727
+ {
728
+ let tmpDrag = document.createElement('div');
729
+ tmpDrag.className = 'retold-remote-collection-item-drag';
730
+ tmpDrag.textContent = '\u2630';
731
+ tmpDrag.draggable = true;
732
+
733
+ tmpDrag.ondragstart = (pEvent) =>
734
+ {
735
+ tmpSelf._draggedItemId = tmpItem.ID;
736
+ tmpRow.classList.add('dragging');
737
+ pEvent.dataTransfer.effectAllowed = 'move';
738
+ };
739
+
740
+ tmpDrag.ondragend = () =>
741
+ {
742
+ tmpRow.classList.remove('dragging');
743
+ tmpSelf._draggedItemId = null;
744
+ // Remove all drag-over classes
745
+ let tmpAllItems = pBody.querySelectorAll('.retold-remote-collection-item');
746
+ for (let j = 0; j < tmpAllItems.length; j++)
747
+ {
748
+ tmpAllItems[j].classList.remove('drag-over');
749
+ }
750
+ };
751
+
752
+ tmpRow.appendChild(tmpDrag);
753
+ }
754
+
755
+ // Drop target events
756
+ tmpRow.ondragover = (pEvent) =>
757
+ {
758
+ pEvent.preventDefault();
759
+ pEvent.dataTransfer.dropEffect = 'move';
760
+ tmpRow.classList.add('drag-over');
761
+ };
762
+ tmpRow.ondragleave = () =>
763
+ {
764
+ tmpRow.classList.remove('drag-over');
765
+ };
766
+ tmpRow.ondrop = (pEvent) =>
767
+ {
768
+ pEvent.preventDefault();
769
+ tmpRow.classList.remove('drag-over');
770
+
771
+ if (tmpSelf._draggedItemId && tmpSelf._draggedItemId !== tmpItem.ID)
772
+ {
773
+ // Build new order: move dragged item before this item
774
+ let tmpCurrentItems = pCollection.Items || [];
775
+ let tmpNewOrder = [];
776
+ for (let j = 0; j < tmpCurrentItems.length; j++)
777
+ {
778
+ if (tmpCurrentItems[j].ID === tmpSelf._draggedItemId)
779
+ {
780
+ continue;
781
+ }
782
+ if (tmpCurrentItems[j].ID === tmpItem.ID)
783
+ {
784
+ tmpNewOrder.push(tmpSelf._draggedItemId);
785
+ }
786
+ tmpNewOrder.push(tmpCurrentItems[j].ID);
787
+ }
788
+ tmpManager.reorderItems(pCollection.GUID, tmpNewOrder);
789
+ }
790
+ };
791
+
792
+ // Item icon/thumbnail
793
+ let tmpIconDiv = document.createElement('div');
794
+ tmpIconDiv.className = 'retold-remote-collection-item-icon';
795
+
796
+ let tmpPath = tmpItem.Path || '';
797
+ let tmpExt = tmpPath.replace(/^.*\./, '').toLowerCase();
798
+ let tmpMediaType = this.pict.PictApplication._getMediaType(tmpExt);
799
+
800
+ if (tmpMediaType === 'image' && tmpItem.Type === 'file')
801
+ {
802
+ let tmpImg = document.createElement('img');
803
+ let tmpMediaProvider = this.pict.providers['RetoldRemote-Provider'];
804
+ if (tmpMediaProvider)
805
+ {
806
+ tmpImg.src = tmpMediaProvider.getThumbnailURL(tmpPath, 48, 48);
807
+ }
808
+ tmpImg.alt = '';
809
+ tmpImg.loading = 'lazy';
810
+ tmpIconDiv.appendChild(tmpImg);
811
+ }
812
+ else
813
+ {
814
+ tmpIconDiv.textContent = this._getTypeIcon(tmpItem.Type, tmpMediaType);
815
+ }
816
+
817
+ // Item name
818
+ let tmpNameDiv = document.createElement('div');
819
+ tmpNameDiv.className = 'retold-remote-collection-item-name';
820
+ tmpNameDiv.textContent = tmpItem.Label || tmpPath.split('/').pop() || tmpPath;
821
+ tmpNameDiv.title = tmpPath;
822
+
823
+ // Type badge
824
+ let tmpTypeDiv = document.createElement('div');
825
+ tmpTypeDiv.className = 'retold-remote-collection-item-type';
826
+ tmpTypeDiv.textContent = tmpItem.Type || 'file';
827
+
828
+ // Remove button
829
+ let tmpRemoveBtn = document.createElement('button');
830
+ tmpRemoveBtn.className = 'retold-remote-collection-item-remove';
831
+ tmpRemoveBtn.title = 'Remove from collection';
832
+ tmpRemoveBtn.textContent = '\u00d7';
833
+ tmpRemoveBtn.onclick = (pEvent) =>
834
+ {
835
+ pEvent.stopPropagation();
836
+ tmpManager.removeItemFromCollection(pCollection.GUID, tmpItem.ID);
837
+ };
838
+
839
+ // Click to navigate — set up collection browsing context
840
+ let tmpItemIndex = i;
841
+ tmpRow.onclick = () =>
842
+ {
843
+ if (tmpItem.Type === 'file' || tmpItem.Type === 'subfile' || tmpItem.Type === 'image-crop' ||
844
+ tmpItem.Type === 'video-clip' || tmpItem.Type === 'video-frame')
845
+ {
846
+ // Enter collection browsing mode so next/prev navigate through collection items
847
+ tmpRemote.BrowsingCollection = true;
848
+ tmpRemote.BrowsingCollectionIndex = tmpItemIndex;
849
+ tmpSelf.pict.PictApplication.navigateToFile(tmpItem.Path);
850
+ }
851
+ else if (tmpItem.Type === 'folder' || tmpItem.Type === 'folder-contents')
852
+ {
853
+ tmpSelf.pict.PictApplication.loadFileList(tmpItem.Path);
854
+ }
855
+ };
856
+
857
+ tmpRow.appendChild(tmpIconDiv);
858
+ tmpRow.appendChild(tmpNameDiv);
859
+ tmpRow.appendChild(tmpTypeDiv);
860
+ tmpRow.appendChild(tmpRemoveBtn);
861
+ pBody.appendChild(tmpRow);
862
+ }
863
+ }
864
+
865
+ // -- Edit Mode --------------------------------------------------------
866
+
867
+ _renderEditMode(pRoot)
868
+ {
869
+ let tmpSelf = this;
870
+ let tmpRemote = this.pict.AppData.RetoldRemote;
871
+ let tmpManager = this.pict.providers['RetoldRemote-CollectionManager'];
872
+ let tmpCollection = tmpRemote.ActiveCollection;
873
+
874
+ pRoot.innerHTML = '';
875
+
876
+ if (!tmpCollection)
877
+ {
878
+ pRoot.innerHTML = '<div class="retold-remote-collections-empty">No collection selected.</div>';
879
+ return;
880
+ }
881
+
882
+ // Header
883
+ let tmpHeader = document.createElement('div');
884
+ tmpHeader.className = 'retold-remote-collections-header';
885
+
886
+ let tmpBackBtn = document.createElement('button');
887
+ tmpBackBtn.className = 'retold-remote-collections-header-btn';
888
+ tmpBackBtn.title = 'Back to detail';
889
+ tmpBackBtn.textContent = '\u2190';
890
+ tmpBackBtn.onclick = () =>
891
+ {
892
+ tmpRemote.CollectionsPanelMode = 'detail';
893
+ tmpSelf.renderContent();
894
+ };
895
+
896
+ let tmpTitle = document.createElement('div');
897
+ tmpTitle.className = 'retold-remote-collections-header-title';
898
+ tmpTitle.textContent = 'Edit Collection';
899
+
900
+ tmpHeader.appendChild(tmpBackBtn);
901
+ tmpHeader.appendChild(tmpTitle);
902
+ pRoot.appendChild(tmpHeader);
903
+
904
+ // Edit form
905
+ let tmpBody = document.createElement('div');
906
+ tmpBody.className = 'retold-remote-collections-body';
907
+
908
+ let tmpForm = document.createElement('div');
909
+ tmpForm.className = 'retold-remote-collections-edit';
910
+
911
+ // Name
912
+ tmpForm.appendChild(this._createEditGroup('Name', 'input', tmpCollection.Name || '', 'edit-name'));
913
+
914
+ // Description
915
+ tmpForm.appendChild(this._createEditGroup('Description (Markdown)', 'textarea', tmpCollection.Description || '', 'edit-description'));
916
+
917
+ // Cover Image
918
+ let tmpCoverGroup = this._createEditGroup('Cover Image Path', 'input', tmpCollection.CoverImage || '', 'edit-cover');
919
+
920
+ // "Use current file" button
921
+ let tmpCurrentFile = this.pict.AppData.ContentEditor.CurrentFile;
922
+ if (tmpCurrentFile)
923
+ {
924
+ let tmpUseCurrent = document.createElement('button');
925
+ tmpUseCurrent.className = 'retold-remote-collections-edit-btn';
926
+ tmpUseCurrent.textContent = 'Use current file';
927
+ tmpUseCurrent.style.marginTop = '4px';
928
+ tmpUseCurrent.style.fontSize = '0.72rem';
929
+ tmpUseCurrent.onclick = () =>
930
+ {
931
+ let tmpInput = document.getElementById('retold-remote-edit-cover');
932
+ if (tmpInput)
933
+ {
934
+ tmpInput.value = tmpCurrentFile;
935
+ }
936
+ };
937
+ tmpCoverGroup.appendChild(tmpUseCurrent);
938
+ }
939
+ tmpForm.appendChild(tmpCoverGroup);
940
+
941
+ // Tags
942
+ tmpForm.appendChild(this._createEditGroup('Tags (comma-separated)', 'input', (tmpCollection.Tags || []).join(', '), 'edit-tags'));
943
+
944
+ // Action buttons
945
+ let tmpActions = document.createElement('div');
946
+ tmpActions.className = 'retold-remote-collections-edit-actions';
947
+
948
+ let tmpSaveBtn = document.createElement('button');
949
+ tmpSaveBtn.className = 'retold-remote-collections-edit-btn retold-remote-collections-edit-btn-primary';
950
+ tmpSaveBtn.textContent = 'Save';
951
+ tmpSaveBtn.onclick = () =>
952
+ {
953
+ let tmpNameInput = document.getElementById('retold-remote-edit-name');
954
+ let tmpDescInput = document.getElementById('retold-remote-edit-description');
955
+ let tmpCoverInput = document.getElementById('retold-remote-edit-cover');
956
+ let tmpTagsInput = document.getElementById('retold-remote-edit-tags');
957
+
958
+ let tmpUpdated =
959
+ {
960
+ GUID: tmpCollection.GUID,
961
+ Name: tmpNameInput ? tmpNameInput.value : tmpCollection.Name,
962
+ Description: tmpDescInput ? tmpDescInput.value : tmpCollection.Description,
963
+ CoverImage: tmpCoverInput ? tmpCoverInput.value : tmpCollection.CoverImage,
964
+ Tags: tmpTagsInput ? tmpTagsInput.value.split(',').map((t) => t.trim()).filter((t) => t) : tmpCollection.Tags
965
+ };
966
+
967
+ tmpManager.updateCollection(tmpUpdated, (pError) =>
968
+ {
969
+ if (!pError)
970
+ {
971
+ tmpRemote.CollectionsPanelMode = 'detail';
972
+ tmpManager.fetchCollection(tmpCollection.GUID);
973
+
974
+ let tmpToast = tmpManager._getToast();
975
+ if (tmpToast)
976
+ {
977
+ tmpToast.show('Collection saved');
978
+ }
979
+ }
980
+ });
981
+ };
982
+
983
+ let tmpCancelBtn = document.createElement('button');
984
+ tmpCancelBtn.className = 'retold-remote-collections-edit-btn';
985
+ tmpCancelBtn.textContent = 'Cancel';
986
+ tmpCancelBtn.onclick = () =>
987
+ {
988
+ tmpRemote.CollectionsPanelMode = 'detail';
989
+ tmpSelf.renderContent();
990
+ };
991
+
992
+ tmpActions.appendChild(tmpSaveBtn);
993
+ tmpActions.appendChild(tmpCancelBtn);
994
+ tmpForm.appendChild(tmpActions);
995
+
996
+ // Delete button
997
+ let tmpDeleteBtn = document.createElement('button');
998
+ tmpDeleteBtn.className = 'retold-remote-collections-edit-btn retold-remote-collections-edit-btn-danger';
999
+ tmpDeleteBtn.textContent = 'Delete Collection';
1000
+ tmpDeleteBtn.onclick = () =>
1001
+ {
1002
+ if (confirm('Delete this collection? This cannot be undone.'))
1003
+ {
1004
+ tmpManager.deleteCollection(tmpCollection.GUID);
1005
+ }
1006
+ };
1007
+ tmpForm.appendChild(tmpDeleteBtn);
1008
+
1009
+ tmpBody.appendChild(tmpForm);
1010
+ pRoot.appendChild(tmpBody);
1011
+ }
1012
+
1013
+ /**
1014
+ * Create a labelled form group (label + input/textarea).
1015
+ */
1016
+ _createEditGroup(pLabel, pType, pValue, pId)
1017
+ {
1018
+ let tmpGroup = document.createElement('div');
1019
+ tmpGroup.className = 'retold-remote-collections-edit-group';
1020
+
1021
+ let tmpLabel = document.createElement('div');
1022
+ tmpLabel.className = 'retold-remote-collections-edit-label';
1023
+ tmpLabel.textContent = pLabel;
1024
+
1025
+ let tmpInput;
1026
+ if (pType === 'textarea')
1027
+ {
1028
+ tmpInput = document.createElement('textarea');
1029
+ tmpInput.className = 'retold-remote-collections-edit-textarea';
1030
+ }
1031
+ else
1032
+ {
1033
+ tmpInput = document.createElement('input');
1034
+ tmpInput.className = 'retold-remote-collections-edit-input';
1035
+ tmpInput.type = 'text';
1036
+ }
1037
+
1038
+ tmpInput.value = pValue;
1039
+ tmpInput.id = 'retold-remote-' + pId;
1040
+
1041
+ tmpGroup.appendChild(tmpLabel);
1042
+ tmpGroup.appendChild(tmpInput);
1043
+ return tmpGroup;
1044
+ }
1045
+
1046
+ // -- Helpers ----------------------------------------------------------
1047
+
1048
+ /**
1049
+ * Get a text icon character for an item type.
1050
+ */
1051
+ _getTypeIcon(pType, pMediaType)
1052
+ {
1053
+ switch (pType)
1054
+ {
1055
+ case 'folder':
1056
+ case 'folder-contents':
1057
+ return '\uD83D\uDCC1';
1058
+ case 'subfile':
1059
+ return '\uD83D\uDDC4';
1060
+ case 'image-crop':
1061
+ return '\u2702';
1062
+ case 'video-clip':
1063
+ case 'video-frame':
1064
+ return '\uD83C\uDFAC';
1065
+ default:
1066
+ break;
1067
+ }
1068
+
1069
+ switch (pMediaType)
1070
+ {
1071
+ case 'image':
1072
+ return '\uD83D\uDDBC';
1073
+ case 'video':
1074
+ return '\uD83C\uDFAC';
1075
+ case 'audio':
1076
+ return '\uD83C\uDFB5';
1077
+ case 'document':
1078
+ return '\uD83D\uDCC4';
1079
+ default:
1080
+ return '\uD83D\uDCC4';
1081
+ }
1082
+ }
1083
+ }
1084
+
1085
+ RetoldRemoteCollectionsPanelView.default_configuration = _ViewConfiguration;
1086
+
1087
+ module.exports = RetoldRemoteCollectionsPanelView;