retold-remote 0.0.1

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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/css/retold-remote.css +83 -0
  3. package/html/codejar.js +511 -0
  4. package/html/index.html +23 -0
  5. package/package.json +68 -0
  6. package/server.js +43 -0
  7. package/source/Pict-Application-RetoldRemote-Configuration.json +7 -0
  8. package/source/Pict-Application-RetoldRemote.js +622 -0
  9. package/source/Pict-RetoldRemote-Bundle.js +14 -0
  10. package/source/cli/RetoldRemote-CLI-Program.js +15 -0
  11. package/source/cli/RetoldRemote-CLI-Run.js +3 -0
  12. package/source/cli/RetoldRemote-Server-Setup.js +257 -0
  13. package/source/cli/commands/RetoldRemote-Command-Serve.js +87 -0
  14. package/source/providers/Pict-Provider-GalleryFilterSort.js +597 -0
  15. package/source/providers/Pict-Provider-GalleryNavigation.js +819 -0
  16. package/source/providers/Pict-Provider-RetoldRemote.js +273 -0
  17. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +640 -0
  18. package/source/providers/Pict-Provider-RetoldRemoteTheme.js +879 -0
  19. package/source/server/RetoldRemote-MediaService.js +536 -0
  20. package/source/server/RetoldRemote-PathRegistry.js +121 -0
  21. package/source/server/RetoldRemote-ThumbnailCache.js +89 -0
  22. package/source/server/RetoldRemote-ToolDetector.js +78 -0
  23. package/source/views/PictView-Remote-Gallery.js +1437 -0
  24. package/source/views/PictView-Remote-ImageViewer.js +363 -0
  25. package/source/views/PictView-Remote-Layout.js +420 -0
  26. package/source/views/PictView-Remote-MediaViewer.js +530 -0
  27. package/source/views/PictView-Remote-SettingsPanel.js +318 -0
  28. package/source/views/PictView-Remote-TopBar.js +206 -0
  29. package/web-application/codejar.js +511 -0
  30. package/web-application/css/retold-remote.css +83 -0
  31. package/web-application/index.html +23 -0
  32. package/web-application/js/pict.min.js +12 -0
  33. package/web-application/js/pict.min.js.map +1 -0
  34. package/web-application/retold-remote.compatible.js +5764 -0
  35. package/web-application/retold-remote.compatible.js.map +1 -0
  36. package/web-application/retold-remote.compatible.min.js +120 -0
  37. package/web-application/retold-remote.compatible.min.js.map +1 -0
  38. package/web-application/retold-remote.js +5763 -0
  39. package/web-application/retold-remote.js.map +1 -0
  40. package/web-application/retold-remote.min.js +120 -0
  41. package/web-application/retold-remote.min.js.map +1 -0
@@ -0,0 +1,1437 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "RetoldRemote-Gallery",
6
+
7
+ DefaultRenderable: "RetoldRemote-Gallery-Grid",
8
+ DefaultDestinationAddress: "#RetoldRemote-Gallery-Container",
9
+
10
+ AutoRender: false,
11
+
12
+ CSS: /*css*/`
13
+ .retold-remote-gallery-header
14
+ {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 8px;
18
+ margin-bottom: 8px;
19
+ flex-wrap: wrap;
20
+ }
21
+ .retold-remote-gallery-filter
22
+ {
23
+ display: flex;
24
+ gap: 4px;
25
+ }
26
+ .retold-remote-gallery-filter-btn
27
+ {
28
+ padding: 3px 10px;
29
+ border: 1px solid var(--retold-border);
30
+ border-radius: 3px;
31
+ background: transparent;
32
+ color: var(--retold-text-muted);
33
+ font-size: 0.72rem;
34
+ cursor: pointer;
35
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
36
+ font-family: inherit;
37
+ }
38
+ .retold-remote-gallery-filter-btn:hover
39
+ {
40
+ color: var(--retold-text-secondary);
41
+ border-color: var(--retold-scrollbar-hover);
42
+ }
43
+ .retold-remote-gallery-filter-btn.active
44
+ {
45
+ color: var(--retold-accent);
46
+ border-color: var(--retold-accent);
47
+ background: rgba(128, 128, 128, 0.1);
48
+ }
49
+ .retold-remote-gallery-filter-toggle
50
+ {
51
+ position: relative;
52
+ }
53
+ .retold-remote-gallery-filter-toggle.has-filters
54
+ {
55
+ color: var(--retold-accent);
56
+ border-color: var(--retold-accent);
57
+ }
58
+ .retold-remote-gallery-filter-count
59
+ {
60
+ display: inline-block;
61
+ min-width: 14px;
62
+ height: 14px;
63
+ line-height: 14px;
64
+ padding: 0 3px;
65
+ border-radius: 7px;
66
+ background: var(--retold-accent);
67
+ color: var(--retold-bg-tertiary);
68
+ font-size: 0.58rem;
69
+ font-weight: 700;
70
+ text-align: center;
71
+ margin-left: 4px;
72
+ }
73
+ /* Sort dropdown */
74
+ .retold-remote-gallery-sort
75
+ {
76
+ display: flex;
77
+ align-items: center;
78
+ }
79
+ .retold-remote-gallery-sort-select
80
+ {
81
+ padding: 3px 6px;
82
+ border: 1px solid var(--retold-border);
83
+ border-radius: 3px;
84
+ background: var(--retold-bg-tertiary);
85
+ color: var(--retold-text-muted);
86
+ font-size: 0.72rem;
87
+ cursor: pointer;
88
+ font-family: inherit;
89
+ }
90
+ .retold-remote-gallery-sort-select:focus
91
+ {
92
+ outline: none;
93
+ border-color: var(--retold-accent);
94
+ }
95
+ .retold-remote-gallery-search
96
+ {
97
+ flex: 1;
98
+ max-width: 300px;
99
+ padding: 4px 10px;
100
+ border: 1px solid var(--retold-border);
101
+ border-radius: 3px;
102
+ background: var(--retold-bg-tertiary);
103
+ color: var(--retold-text-primary);
104
+ font-size: 0.78rem;
105
+ font-family: inherit;
106
+ }
107
+ .retold-remote-gallery-search:focus
108
+ {
109
+ outline: none;
110
+ border-color: var(--retold-accent);
111
+ }
112
+ .retold-remote-gallery-search::placeholder
113
+ {
114
+ color: var(--retold-text-placeholder);
115
+ }
116
+ /* Filter chips */
117
+ .retold-remote-filter-chips
118
+ {
119
+ display: flex;
120
+ flex-wrap: wrap;
121
+ gap: 6px;
122
+ margin-bottom: 8px;
123
+ align-items: center;
124
+ }
125
+ .retold-remote-filter-chip
126
+ {
127
+ display: inline-flex;
128
+ align-items: center;
129
+ gap: 4px;
130
+ padding: 2px 8px;
131
+ border: 1px solid var(--retold-border-light);
132
+ border-radius: 12px;
133
+ background: rgba(128, 128, 128, 0.08);
134
+ color: var(--retold-text-secondary);
135
+ font-size: 0.68rem;
136
+ }
137
+ .retold-remote-filter-chip-remove
138
+ {
139
+ background: none;
140
+ border: none;
141
+ color: var(--retold-text-muted);
142
+ font-size: 0.82rem;
143
+ cursor: pointer;
144
+ padding: 0 2px;
145
+ line-height: 1;
146
+ }
147
+ .retold-remote-filter-chip-remove:hover
148
+ {
149
+ color: var(--retold-danger);
150
+ }
151
+ .retold-remote-filter-chip-clear
152
+ {
153
+ background: none;
154
+ border: none;
155
+ color: var(--retold-text-dim);
156
+ font-size: 0.68rem;
157
+ cursor: pointer;
158
+ padding: 2px 4px;
159
+ }
160
+ .retold-remote-filter-chip-clear:hover
161
+ {
162
+ color: var(--retold-danger);
163
+ }
164
+ /* Filter panel */
165
+ .retold-remote-filter-panel
166
+ {
167
+ margin-bottom: 12px;
168
+ padding: 12px;
169
+ border: 1px solid var(--retold-border);
170
+ border-radius: 6px;
171
+ background: var(--retold-bg-panel);
172
+ }
173
+ .retold-remote-filter-panel-grid
174
+ {
175
+ display: grid;
176
+ grid-template-columns: 1fr 1fr;
177
+ gap: 12px 24px;
178
+ }
179
+ .retold-remote-filter-section
180
+ {
181
+ margin-bottom: 0;
182
+ }
183
+ .retold-remote-filter-section-title
184
+ {
185
+ font-size: 0.68rem;
186
+ font-weight: 600;
187
+ color: var(--retold-text-muted);
188
+ text-transform: uppercase;
189
+ letter-spacing: 0.5px;
190
+ margin-bottom: 6px;
191
+ }
192
+ .retold-remote-filter-ext-list
193
+ {
194
+ display: flex;
195
+ flex-wrap: wrap;
196
+ gap: 2px 10px;
197
+ }
198
+ .retold-remote-filter-ext-item
199
+ {
200
+ display: flex;
201
+ align-items: center;
202
+ gap: 4px;
203
+ font-size: 0.72rem;
204
+ color: var(--retold-text-secondary);
205
+ cursor: pointer;
206
+ }
207
+ .retold-remote-filter-ext-item input
208
+ {
209
+ accent-color: var(--retold-accent);
210
+ }
211
+ .retold-remote-filter-ext-count
212
+ {
213
+ color: var(--retold-text-dim);
214
+ font-size: 0.65rem;
215
+ }
216
+ .retold-remote-filter-row
217
+ {
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 6px;
221
+ }
222
+ .retold-remote-filter-input
223
+ {
224
+ padding: 3px 6px;
225
+ border: 1px solid var(--retold-border);
226
+ border-radius: 3px;
227
+ background: var(--retold-bg-tertiary);
228
+ color: var(--retold-text-secondary);
229
+ font-size: 0.72rem;
230
+ width: 100px;
231
+ font-family: inherit;
232
+ }
233
+ .retold-remote-filter-input:focus
234
+ {
235
+ outline: none;
236
+ border-color: var(--retold-accent);
237
+ }
238
+ .retold-remote-filter-label
239
+ {
240
+ color: var(--retold-text-dim);
241
+ font-size: 0.68rem;
242
+ }
243
+ .retold-remote-filter-actions
244
+ {
245
+ grid-column: 1 / -1;
246
+ display: flex;
247
+ gap: 8px;
248
+ align-items: center;
249
+ padding-top: 8px;
250
+ border-top: 1px solid var(--retold-border);
251
+ margin-top: 8px;
252
+ }
253
+ .retold-remote-filter-preset-row
254
+ {
255
+ display: flex;
256
+ gap: 6px;
257
+ align-items: center;
258
+ flex-wrap: wrap;
259
+ }
260
+ .retold-remote-filter-preset-select
261
+ {
262
+ padding: 3px 6px;
263
+ border: 1px solid var(--retold-border);
264
+ border-radius: 3px;
265
+ background: var(--retold-bg-tertiary);
266
+ color: var(--retold-text-secondary);
267
+ font-size: 0.72rem;
268
+ min-width: 100px;
269
+ font-family: inherit;
270
+ }
271
+ .retold-remote-filter-preset-input
272
+ {
273
+ padding: 3px 6px;
274
+ border: 1px solid var(--retold-border);
275
+ border-radius: 3px;
276
+ background: var(--retold-bg-tertiary);
277
+ color: var(--retold-text-secondary);
278
+ font-size: 0.72rem;
279
+ width: 120px;
280
+ font-family: inherit;
281
+ }
282
+ .retold-remote-filter-preset-input:focus
283
+ {
284
+ outline: none;
285
+ border-color: var(--retold-accent);
286
+ }
287
+ .retold-remote-filter-btn-sm
288
+ {
289
+ padding: 2px 8px;
290
+ border: 1px solid var(--retold-border);
291
+ border-radius: 3px;
292
+ background: transparent;
293
+ color: var(--retold-text-muted);
294
+ font-size: 0.68rem;
295
+ cursor: pointer;
296
+ font-family: inherit;
297
+ }
298
+ .retold-remote-filter-btn-sm:hover
299
+ {
300
+ color: var(--retold-text-secondary);
301
+ border-color: var(--retold-scrollbar-hover);
302
+ }
303
+ /* Grid layout */
304
+ .retold-remote-grid
305
+ {
306
+ display: grid;
307
+ gap: 12px;
308
+ }
309
+ .retold-remote-grid.size-small
310
+ {
311
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
312
+ }
313
+ .retold-remote-grid.size-medium
314
+ {
315
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
316
+ }
317
+ .retold-remote-grid.size-large
318
+ {
319
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
320
+ }
321
+ /* Tile */
322
+ .retold-remote-tile
323
+ {
324
+ background: var(--retold-bg-tertiary);
325
+ border: 2px solid transparent;
326
+ border-radius: 6px;
327
+ cursor: pointer;
328
+ overflow: hidden;
329
+ transition: border-color 0.15s, transform 0.1s;
330
+ }
331
+ .retold-remote-tile:hover
332
+ {
333
+ border-color: var(--retold-border-light);
334
+ }
335
+ .retold-remote-tile.selected
336
+ {
337
+ border-color: var(--retold-accent);
338
+ box-shadow: 0 0 0 1px rgba(128, 128, 128, 0.3);
339
+ }
340
+ .retold-remote-tile-thumb
341
+ {
342
+ position: relative;
343
+ width: 100%;
344
+ padding-bottom: 75%; /* 4:3 aspect ratio */
345
+ background: var(--retold-bg-thumb);
346
+ overflow: hidden;
347
+ }
348
+ .retold-remote-tile-thumb img
349
+ {
350
+ position: absolute;
351
+ top: 0;
352
+ left: 0;
353
+ width: 100%;
354
+ height: 100%;
355
+ object-fit: cover;
356
+ }
357
+ .retold-remote-tile-thumb-icon
358
+ {
359
+ position: absolute;
360
+ top: 50%;
361
+ left: 50%;
362
+ transform: translate(-50%, -50%);
363
+ font-size: 2rem;
364
+ color: var(--retold-text-placeholder);
365
+ }
366
+ .retold-remote-tile-badge
367
+ {
368
+ position: absolute;
369
+ top: 6px;
370
+ right: 6px;
371
+ padding: 1px 6px;
372
+ border-radius: 3px;
373
+ font-size: 0.6rem;
374
+ font-weight: 700;
375
+ text-transform: uppercase;
376
+ letter-spacing: 0.5px;
377
+ }
378
+ .retold-remote-tile-badge-image { background: rgba(102, 194, 184, 0.8); color: #fff; }
379
+ .retold-remote-tile-badge-video { background: rgba(180, 102, 194, 0.8); color: #fff; }
380
+ .retold-remote-tile-badge-audio { background: rgba(194, 160, 102, 0.8); color: #fff; }
381
+ .retold-remote-tile-badge-document { background: rgba(194, 102, 102, 0.8); color: #fff; }
382
+ .retold-remote-tile-badge-folder { background: rgba(102, 140, 194, 0.8); color: #fff; }
383
+ .retold-remote-tile-duration
384
+ {
385
+ position: absolute;
386
+ bottom: 6px;
387
+ right: 6px;
388
+ padding: 1px 6px;
389
+ border-radius: 3px;
390
+ background: rgba(0, 0, 0, 0.7);
391
+ font-size: 0.65rem;
392
+ color: var(--retold-text-primary);
393
+ }
394
+ .retold-remote-tile-label
395
+ {
396
+ padding: 6px 8px 2px 8px;
397
+ font-size: 0.75rem;
398
+ font-weight: 500;
399
+ color: var(--retold-text-secondary);
400
+ overflow: hidden;
401
+ text-overflow: ellipsis;
402
+ white-space: nowrap;
403
+ }
404
+ .retold-remote-tile-meta
405
+ {
406
+ padding: 0 8px 6px 8px;
407
+ font-size: 0.65rem;
408
+ color: var(--retold-text-dim);
409
+ }
410
+ /* List mode */
411
+ .retold-remote-list
412
+ {
413
+ display: flex;
414
+ flex-direction: column;
415
+ gap: 2px;
416
+ }
417
+ .retold-remote-list-row
418
+ {
419
+ display: flex;
420
+ align-items: center;
421
+ gap: 12px;
422
+ padding: 6px 12px;
423
+ border-radius: 4px;
424
+ cursor: pointer;
425
+ transition: background 0.1s;
426
+ }
427
+ .retold-remote-list-row:hover
428
+ {
429
+ background: var(--retold-bg-hover);
430
+ }
431
+ .retold-remote-list-row.selected
432
+ {
433
+ background: var(--retold-bg-selected);
434
+ }
435
+ .retold-remote-list-icon
436
+ {
437
+ flex-shrink: 0;
438
+ width: 24px;
439
+ text-align: center;
440
+ color: var(--retold-text-dim);
441
+ }
442
+ .retold-remote-list-name
443
+ {
444
+ flex: 1;
445
+ font-size: 0.82rem;
446
+ color: var(--retold-text-secondary);
447
+ overflow: hidden;
448
+ text-overflow: ellipsis;
449
+ white-space: nowrap;
450
+ }
451
+ .retold-remote-list-size
452
+ {
453
+ flex-shrink: 0;
454
+ width: 80px;
455
+ text-align: right;
456
+ font-size: 0.72rem;
457
+ color: var(--retold-text-dim);
458
+ }
459
+ .retold-remote-list-date
460
+ {
461
+ flex-shrink: 0;
462
+ width: 140px;
463
+ text-align: right;
464
+ font-size: 0.72rem;
465
+ color: var(--retold-text-dim);
466
+ }
467
+ /* Empty state */
468
+ .retold-remote-empty
469
+ {
470
+ text-align: center;
471
+ padding: 60px 20px;
472
+ color: var(--retold-text-dim);
473
+ }
474
+ .retold-remote-empty-icon
475
+ {
476
+ font-size: 3rem;
477
+ margin-bottom: 12px;
478
+ }
479
+ /* Help panel flyout */
480
+ #RetoldRemote-Help-Panel
481
+ {
482
+ position: fixed;
483
+ top: 0;
484
+ left: 0;
485
+ width: 100%;
486
+ height: 100%;
487
+ z-index: 9999;
488
+ }
489
+ .retold-remote-help-backdrop
490
+ {
491
+ position: absolute;
492
+ top: 0;
493
+ left: 0;
494
+ width: 100%;
495
+ height: 100%;
496
+ background: rgba(0, 0, 0, 0.5);
497
+ display: flex;
498
+ align-items: flex-start;
499
+ justify-content: flex-end;
500
+ }
501
+ .retold-remote-help-flyout
502
+ {
503
+ width: 340px;
504
+ max-height: calc(100vh - 32px);
505
+ margin: 16px;
506
+ background: var(--retold-bg-panel);
507
+ border: 1px solid var(--retold-border);
508
+ border-radius: 8px;
509
+ overflow-y: auto;
510
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
511
+ animation: retold-help-slide-in 0.15s ease-out;
512
+ }
513
+ @keyframes retold-help-slide-in
514
+ {
515
+ from { transform: translateX(20px); opacity: 0; }
516
+ to { transform: translateX(0); opacity: 1; }
517
+ }
518
+ .retold-remote-help-header
519
+ {
520
+ display: flex;
521
+ align-items: center;
522
+ justify-content: space-between;
523
+ padding: 14px 16px 10px 16px;
524
+ border-bottom: 1px solid var(--retold-border);
525
+ }
526
+ .retold-remote-help-title
527
+ {
528
+ font-size: 0.88rem;
529
+ font-weight: 600;
530
+ color: var(--retold-text-primary);
531
+ }
532
+ .retold-remote-help-close
533
+ {
534
+ background: none;
535
+ border: none;
536
+ color: var(--retold-text-dim);
537
+ font-size: 1.2rem;
538
+ cursor: pointer;
539
+ padding: 0 4px;
540
+ line-height: 1;
541
+ }
542
+ .retold-remote-help-close:hover
543
+ {
544
+ color: var(--retold-danger);
545
+ }
546
+ .retold-remote-help-section
547
+ {
548
+ padding: 12px 16px 8px 16px;
549
+ }
550
+ .retold-remote-help-section + .retold-remote-help-section
551
+ {
552
+ border-top: 1px solid var(--retold-border);
553
+ }
554
+ .retold-remote-help-section-title
555
+ {
556
+ font-size: 0.65rem;
557
+ font-weight: 600;
558
+ color: var(--retold-accent);
559
+ text-transform: uppercase;
560
+ letter-spacing: 0.8px;
561
+ margin-bottom: 8px;
562
+ }
563
+ .retold-remote-help-row
564
+ {
565
+ display: flex;
566
+ align-items: center;
567
+ gap: 12px;
568
+ padding: 3px 0;
569
+ }
570
+ .retold-remote-help-key
571
+ {
572
+ display: inline-block;
573
+ min-width: 72px;
574
+ padding: 2px 8px;
575
+ border: 1px solid var(--retold-border-light);
576
+ border-radius: 4px;
577
+ background: rgba(255, 255, 255, 0.04);
578
+ color: var(--retold-text-secondary);
579
+ font-family: var(--retold-font-mono, monospace);
580
+ font-size: 0.72rem;
581
+ text-align: center;
582
+ white-space: nowrap;
583
+ }
584
+ .retold-remote-help-desc
585
+ {
586
+ color: var(--retold-text-muted);
587
+ font-size: 0.74rem;
588
+ }
589
+ .retold-remote-help-footer
590
+ {
591
+ padding: 10px 16px;
592
+ border-top: 1px solid var(--retold-border);
593
+ font-size: 0.68rem;
594
+ color: var(--retold-text-dim);
595
+ text-align: center;
596
+ }
597
+ .retold-remote-help-footer strong
598
+ {
599
+ color: var(--retold-accent);
600
+ }
601
+ `,
602
+
603
+ Templates: [],
604
+ Renderables: []
605
+ };
606
+
607
+ class RetoldRemoteGalleryView extends libPictView
608
+ {
609
+ constructor(pFable, pOptions, pServiceHash)
610
+ {
611
+ super(pFable, pOptions, pServiceHash);
612
+
613
+ this._intersectionObserver = null;
614
+ }
615
+
616
+ // ──────────────────────────────────────────────
617
+ // Gallery rendering
618
+ // ──────────────────────────────────────────────
619
+
620
+ /**
621
+ * Render the gallery based on current state.
622
+ * GalleryItems is already filtered+sorted by the pipeline provider.
623
+ */
624
+ renderGallery()
625
+ {
626
+ let tmpContainer = document.getElementById('RetoldRemote-Gallery-Container');
627
+ if (!tmpContainer)
628
+ {
629
+ return;
630
+ }
631
+
632
+ let tmpRemote = this.pict.AppData.RetoldRemote;
633
+ let tmpItems = tmpRemote.GalleryItems || [];
634
+ let tmpViewMode = tmpRemote.ViewMode || 'list';
635
+ let tmpThumbnailSize = tmpRemote.ThumbnailSize || 'medium';
636
+ let tmpCursorIndex = tmpRemote.GalleryCursorIndex || 0;
637
+
638
+ // Build header with type filters, sort, filter toggle, and search
639
+ let tmpHTML = this._buildHeaderHTML(tmpRemote.FilterState ? tmpRemote.FilterState.MediaType : 'all');
640
+
641
+ // Build collapsible filter panel
642
+ tmpHTML += this._buildFilterPanelHTML();
643
+
644
+ // Build filter chips bar
645
+ tmpHTML += this._buildFilterChipsHTML();
646
+
647
+ if (tmpItems.length === 0)
648
+ {
649
+ tmpHTML += '<div class="retold-remote-empty">';
650
+ let tmpEmptyIconProvider = this.pict.providers['RetoldRemote-Icons'];
651
+ tmpHTML += '<div class="retold-remote-empty-icon"><span class="retold-remote-icon retold-remote-icon-xl">' + (tmpEmptyIconProvider ? tmpEmptyIconProvider.getIcon('gallery-empty', 96) : '') + '</span></div>';
652
+ tmpHTML += '<div>Empty folder</div>';
653
+ tmpHTML += '</div>';
654
+ tmpContainer.innerHTML = tmpHTML;
655
+ return;
656
+ }
657
+
658
+ // Items are already filtered+sorted by the pipeline
659
+ if (tmpViewMode === 'gallery')
660
+ {
661
+ tmpHTML += this._buildGridHTML(tmpItems, tmpThumbnailSize, tmpCursorIndex);
662
+ }
663
+ else
664
+ {
665
+ tmpHTML += this._buildListHTML(tmpItems, tmpCursorIndex);
666
+ }
667
+
668
+ tmpContainer.innerHTML = tmpHTML;
669
+
670
+ // Set up lazy loading for thumbnail images
671
+ this._setupLazyLoading();
672
+
673
+ // Recalculate column count for keyboard navigation
674
+ let tmpNavProvider = this.pict.providers['RetoldRemote-GalleryNavigation'];
675
+ if (tmpNavProvider)
676
+ {
677
+ tmpNavProvider.recalculateColumns();
678
+ }
679
+ }
680
+
681
+ // ──────────────────────────────────────────────
682
+ // Header
683
+ // ──────────────────────────────────────────────
684
+
685
+ /**
686
+ * Build the gallery header with type filters, sort dropdown, filter toggle, and search.
687
+ */
688
+ _buildHeaderHTML(pActiveFilter)
689
+ {
690
+ let tmpRemote = this.pict.AppData.RetoldRemote;
691
+ let tmpFilters = [
692
+ { key: 'all', label: 'All' },
693
+ { key: 'images', label: 'Images' },
694
+ { key: 'video', label: 'Video' },
695
+ { key: 'audio', label: 'Audio' },
696
+ { key: 'documents', label: 'Docs' }
697
+ ];
698
+
699
+ let tmpHTML = '<div class="retold-remote-gallery-header">';
700
+
701
+ // Media type filter buttons
702
+ tmpHTML += '<div class="retold-remote-gallery-filter">';
703
+ for (let i = 0; i < tmpFilters.length; i++)
704
+ {
705
+ let tmpFilter = tmpFilters[i];
706
+ let tmpActiveClass = (tmpFilter.key === pActiveFilter) ? ' active' : '';
707
+ tmpHTML += '<button class="retold-remote-gallery-filter-btn' + tmpActiveClass + '" '
708
+ + 'onclick="pict.views[\'RetoldRemote-Gallery\'].setFilter(\'' + tmpFilter.key + '\')">'
709
+ + tmpFilter.label + '</button>';
710
+ }
711
+ tmpHTML += '</div>';
712
+
713
+ // Sort dropdown
714
+ tmpHTML += '<div class="retold-remote-gallery-sort">';
715
+ tmpHTML += '<select class="retold-remote-gallery-sort-select" id="RetoldRemote-Gallery-Sort" '
716
+ + 'onchange="pict.views[\'RetoldRemote-Gallery\'].onSortChange(this.value)">';
717
+ let tmpSortOptions = [
718
+ { value: 'folder-first:asc', label: 'Folders first' },
719
+ { value: 'name:asc', label: 'Name A\u2013Z' },
720
+ { value: 'name:desc', label: 'Name Z\u2013A' },
721
+ { value: 'modified:desc', label: 'Newest modified' },
722
+ { value: 'modified:asc', label: 'Oldest modified' },
723
+ { value: 'created:desc', label: 'Newest created' },
724
+ { value: 'created:asc', label: 'Oldest created' }
725
+ ];
726
+ let tmpCurrentSort = (tmpRemote.SortField || 'folder-first') + ':' + (tmpRemote.SortDirection || 'asc');
727
+ for (let i = 0; i < tmpSortOptions.length; i++)
728
+ {
729
+ let tmpSelected = (tmpSortOptions[i].value === tmpCurrentSort) ? ' selected' : '';
730
+ tmpHTML += '<option value="' + tmpSortOptions[i].value + '"' + tmpSelected + '>'
731
+ + tmpSortOptions[i].label + '</option>';
732
+ }
733
+ tmpHTML += '</select>';
734
+ tmpHTML += '</div>';
735
+
736
+ // Filter panel toggle button
737
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
738
+ let tmpActiveChipCount = tmpFilterSort ? tmpFilterSort.getActiveFilterChips().length : 0;
739
+ // Don't count search chip in the toggle badge since it's obvious from the search input
740
+ let tmpBadgeCount = tmpActiveChipCount;
741
+ let tmpHasFiltersClass = tmpBadgeCount > 0 ? ' has-filters' : '';
742
+ tmpHTML += '<button class="retold-remote-gallery-filter-btn retold-remote-gallery-filter-toggle' + tmpHasFiltersClass + '" '
743
+ + 'onclick="pict.views[\'RetoldRemote-Gallery\'].toggleFilterPanel()">'
744
+ + '\u2699 Filters';
745
+ if (tmpBadgeCount > 0)
746
+ {
747
+ tmpHTML += '<span class="retold-remote-gallery-filter-count">' + tmpBadgeCount + '</span>';
748
+ }
749
+ tmpHTML += '</button>';
750
+
751
+ // Search input
752
+ let tmpSearchValue = tmpRemote.SearchQuery || '';
753
+ tmpHTML += '<input type="text" class="retold-remote-gallery-search" id="RetoldRemote-Gallery-Search" '
754
+ + 'placeholder="Search files... (/)" '
755
+ + 'value="' + this._escapeHTML(tmpSearchValue) + '" '
756
+ + 'oninput="pict.views[\'RetoldRemote-Gallery\'].onSearchInput(this.value)">';
757
+ tmpHTML += '</div>';
758
+
759
+ return tmpHTML;
760
+ }
761
+
762
+ // ──────────────────────────────────────────────
763
+ // Filter panel (collapsible)
764
+ // ──────────────────────────────────────────────
765
+
766
+ /**
767
+ * Build the collapsible advanced filter panel HTML.
768
+ */
769
+ _buildFilterPanelHTML()
770
+ {
771
+ let tmpRemote = this.pict.AppData.RetoldRemote;
772
+ if (!tmpRemote.FilterPanelOpen)
773
+ {
774
+ return '';
775
+ }
776
+
777
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
778
+ let tmpAvailableExtensions = tmpFilterSort ? tmpFilterSort.getAvailableExtensions() : [];
779
+ let tmpFilterState = tmpRemote.FilterState || {};
780
+
781
+ let tmpHTML = '<div class="retold-remote-filter-panel">';
782
+ tmpHTML += '<div class="retold-remote-filter-panel-grid">';
783
+
784
+ // Extension filter: checkboxes for each extension in the current folder
785
+ tmpHTML += '<div class="retold-remote-filter-section">';
786
+ tmpHTML += '<div class="retold-remote-filter-section-title">File Type</div>';
787
+ tmpHTML += '<div class="retold-remote-filter-ext-list">';
788
+ let tmpActiveExtensions = tmpFilterState.Extensions || [];
789
+ for (let i = 0; i < tmpAvailableExtensions.length; i++)
790
+ {
791
+ let tmpExt = tmpAvailableExtensions[i];
792
+ // If Extensions is empty, all are selected
793
+ let tmpChecked = (tmpActiveExtensions.length === 0 || tmpActiveExtensions.indexOf(tmpExt.ext) >= 0);
794
+ tmpHTML += '<label class="retold-remote-filter-ext-item">';
795
+ tmpHTML += '<input type="checkbox" ' + (tmpChecked ? 'checked ' : '')
796
+ + 'onchange="pict.views[\'RetoldRemote-Gallery\'].onExtensionToggle(\'' + tmpExt.ext + '\', this.checked)">';
797
+ tmpHTML += ' .' + tmpExt.ext + ' <span class="retold-remote-filter-ext-count">(' + tmpExt.count + ')</span>';
798
+ tmpHTML += '</label>';
799
+ }
800
+ tmpHTML += '</div>';
801
+ tmpHTML += '</div>';
802
+
803
+ // Size range
804
+ tmpHTML += '<div class="retold-remote-filter-section">';
805
+ tmpHTML += '<div class="retold-remote-filter-section-title">File Size</div>';
806
+ tmpHTML += '<div class="retold-remote-filter-row">';
807
+ let tmpMinKB = (tmpFilterState.SizeMin !== null && tmpFilterState.SizeMin !== undefined) ? Math.round(tmpFilterState.SizeMin / 1024) : '';
808
+ let tmpMaxKB = (tmpFilterState.SizeMax !== null && tmpFilterState.SizeMax !== undefined) ? Math.round(tmpFilterState.SizeMax / 1024) : '';
809
+ tmpHTML += '<input type="number" class="retold-remote-filter-input" placeholder="Min KB" '
810
+ + 'value="' + tmpMinKB + '" '
811
+ + 'onchange="pict.views[\'RetoldRemote-Gallery\'].onSizeFilterChange(\'min\', this.value)">';
812
+ tmpHTML += '<span class="retold-remote-filter-label">to</span>';
813
+ tmpHTML += '<input type="number" class="retold-remote-filter-input" placeholder="Max KB" '
814
+ + 'value="' + tmpMaxKB + '" '
815
+ + 'onchange="pict.views[\'RetoldRemote-Gallery\'].onSizeFilterChange(\'max\', this.value)">';
816
+ tmpHTML += '<span class="retold-remote-filter-label">KB</span>';
817
+ tmpHTML += '</div>';
818
+ tmpHTML += '</div>';
819
+
820
+ // Modified date range
821
+ tmpHTML += '<div class="retold-remote-filter-section">';
822
+ tmpHTML += '<div class="retold-remote-filter-section-title">Modified Date</div>';
823
+ tmpHTML += '<div class="retold-remote-filter-row">';
824
+ tmpHTML += '<input type="date" class="retold-remote-filter-input" '
825
+ + 'value="' + (tmpFilterState.DateModifiedAfter || '') + '" '
826
+ + 'onchange="pict.views[\'RetoldRemote-Gallery\'].onDateFilterChange(\'DateModifiedAfter\', this.value)">';
827
+ tmpHTML += '<span class="retold-remote-filter-label">to</span>';
828
+ tmpHTML += '<input type="date" class="retold-remote-filter-input" '
829
+ + 'value="' + (tmpFilterState.DateModifiedBefore || '') + '" '
830
+ + 'onchange="pict.views[\'RetoldRemote-Gallery\'].onDateFilterChange(\'DateModifiedBefore\', this.value)">';
831
+ tmpHTML += '</div>';
832
+ tmpHTML += '</div>';
833
+
834
+ // Presets
835
+ tmpHTML += '<div class="retold-remote-filter-section">';
836
+ tmpHTML += '<div class="retold-remote-filter-section-title">Presets</div>';
837
+ tmpHTML += this._buildPresetControlsHTML();
838
+ tmpHTML += '</div>';
839
+
840
+ // Actions row
841
+ tmpHTML += '<div class="retold-remote-filter-actions">';
842
+ tmpHTML += '<button class="retold-remote-filter-btn-sm" onclick="pict.views[\'RetoldRemote-Gallery\'].clearAllFilters()">Clear All Filters</button>';
843
+ tmpHTML += '</div>';
844
+
845
+ tmpHTML += '</div>'; // end grid
846
+ tmpHTML += '</div>'; // end panel
847
+
848
+ return tmpHTML;
849
+ }
850
+
851
+ /**
852
+ * Build preset controls (save/load/delete).
853
+ */
854
+ _buildPresetControlsHTML()
855
+ {
856
+ let tmpRemote = this.pict.AppData.RetoldRemote;
857
+ let tmpPresets = tmpRemote.FilterPresets || [];
858
+
859
+ let tmpHTML = '<div class="retold-remote-filter-preset-row">';
860
+
861
+ // Load preset dropdown
862
+ if (tmpPresets.length > 0)
863
+ {
864
+ tmpHTML += '<select class="retold-remote-filter-preset-select" id="RetoldRemote-Filter-PresetSelect" '
865
+ + 'onchange="pict.views[\'RetoldRemote-Gallery\'].loadFilterPreset(this.value)">';
866
+ tmpHTML += '<option value="">Load preset...</option>';
867
+ for (let i = 0; i < tmpPresets.length; i++)
868
+ {
869
+ tmpHTML += '<option value="' + i + '">' + this._escapeHTML(tmpPresets[i].Name) + '</option>';
870
+ }
871
+ tmpHTML += '</select>';
872
+ tmpHTML += '<button class="retold-remote-filter-btn-sm" onclick="pict.views[\'RetoldRemote-Gallery\'].deleteSelectedPreset()">\u2715</button>';
873
+ }
874
+
875
+ // Save new preset
876
+ tmpHTML += '<input type="text" class="retold-remote-filter-preset-input" id="RetoldRemote-Filter-PresetName" '
877
+ + 'placeholder="Preset name...">';
878
+ tmpHTML += '<button class="retold-remote-filter-btn-sm" onclick="pict.views[\'RetoldRemote-Gallery\'].saveFilterPreset()">Save</button>';
879
+
880
+ tmpHTML += '</div>';
881
+ return tmpHTML;
882
+ }
883
+
884
+ // ──────────────────────────────────────────────
885
+ // Filter chips
886
+ // ──────────────────────────────────────────────
887
+
888
+ /**
889
+ * Build filter chips bar showing all active filters.
890
+ */
891
+ _buildFilterChipsHTML()
892
+ {
893
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
894
+ if (!tmpFilterSort)
895
+ {
896
+ return '';
897
+ }
898
+
899
+ let tmpChips = tmpFilterSort.getActiveFilterChips();
900
+ if (tmpChips.length === 0)
901
+ {
902
+ return '';
903
+ }
904
+
905
+ let tmpHTML = '<div class="retold-remote-filter-chips">';
906
+ for (let i = 0; i < tmpChips.length; i++)
907
+ {
908
+ let tmpChip = tmpChips[i];
909
+ tmpHTML += '<span class="retold-remote-filter-chip">'
910
+ + this._escapeHTML(tmpChip.label)
911
+ + ' <button class="retold-remote-filter-chip-remove" '
912
+ + 'onclick="pict.views[\'RetoldRemote-Gallery\'].removeFilterChip(\'' + this._escapeHTML(tmpChip.key) + '\')">&times;</button>'
913
+ + '</span>';
914
+ }
915
+ tmpHTML += '<button class="retold-remote-filter-chip-clear" onclick="pict.views[\'RetoldRemote-Gallery\'].clearAllFilters()">Clear all</button>';
916
+ tmpHTML += '</div>';
917
+ return tmpHTML;
918
+ }
919
+
920
+ // ──────────────────────────────────────────────
921
+ // Grid and list HTML builders
922
+ // ──────────────────────────────────────────────
923
+
924
+ /**
925
+ * Build the grid view HTML.
926
+ */
927
+ _buildGridHTML(pItems, pThumbnailSize, pCursorIndex)
928
+ {
929
+ let tmpHTML = '<div class="retold-remote-grid size-' + pThumbnailSize + '">';
930
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
931
+ let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
932
+
933
+ for (let i = 0; i < pItems.length; i++)
934
+ {
935
+ let tmpItem = pItems[i];
936
+ let tmpSelectedClass = (i === pCursorIndex) ? ' selected' : '';
937
+ let tmpExtension = (tmpItem.Extension || '').toLowerCase();
938
+ let tmpCategory = this._getCategory(tmpExtension, tmpItem.Type);
939
+
940
+ tmpHTML += '<div class="retold-remote-tile' + tmpSelectedClass + '" '
941
+ + 'data-index="' + i + '" '
942
+ + 'onclick="pict.views[\'RetoldRemote-Gallery\'].onTileClick(' + i + ')" '
943
+ + 'ondblclick="pict.views[\'RetoldRemote-Gallery\'].onTileDoubleClick(' + i + ')">';
944
+
945
+ // Thumbnail area
946
+ tmpHTML += '<div class="retold-remote-tile-thumb">';
947
+
948
+ if (tmpItem.Type === 'folder')
949
+ {
950
+ tmpHTML += '<div class="retold-remote-tile-thumb-icon"><span class="retold-remote-icon retold-remote-icon-md">' + (tmpIconProvider ? tmpIconProvider.getIcon('folder', 48) : '') + '</span></div>';
951
+ tmpHTML += '<span class="retold-remote-tile-badge retold-remote-tile-badge-folder">Folder</span>';
952
+ }
953
+ else if (tmpCategory === 'image' && tmpProvider)
954
+ {
955
+ let tmpThumbURL = tmpProvider.getThumbnailURL(tmpItem.Path, 400, 300);
956
+ if (tmpThumbURL)
957
+ {
958
+ tmpHTML += '<img data-src="' + tmpThumbURL + '" alt="' + this._escapeHTML(tmpItem.Name) + '" loading="lazy">';
959
+ }
960
+ else
961
+ {
962
+ tmpHTML += '<div class="retold-remote-tile-thumb-icon"><span class="retold-remote-icon retold-remote-icon-md">' + (tmpIconProvider ? tmpIconProvider.getIcon('file-image', 48) : '') + '</span></div>';
963
+ }
964
+ tmpHTML += '<span class="retold-remote-tile-badge retold-remote-tile-badge-image">' + tmpExtension + '</span>';
965
+ }
966
+ else if (tmpCategory === 'video')
967
+ {
968
+ if (tmpProvider)
969
+ {
970
+ let tmpThumbURL = tmpProvider.getThumbnailURL(tmpItem.Path, 400, 300);
971
+ if (tmpThumbURL)
972
+ {
973
+ tmpHTML += '<img data-src="' + tmpThumbURL + '" alt="' + this._escapeHTML(tmpItem.Name) + '" loading="lazy">';
974
+ }
975
+ else
976
+ {
977
+ tmpHTML += '<div class="retold-remote-tile-thumb-icon"><span class="retold-remote-icon retold-remote-icon-md">' + (tmpIconProvider ? tmpIconProvider.getIcon('file-video', 48) : '') + '</span></div>';
978
+ }
979
+ }
980
+ else
981
+ {
982
+ tmpHTML += '<div class="retold-remote-tile-thumb-icon"><span class="retold-remote-icon retold-remote-icon-md">' + (tmpIconProvider ? tmpIconProvider.getIcon('file-video', 48) : '') + '</span></div>';
983
+ }
984
+ tmpHTML += '<span class="retold-remote-tile-badge retold-remote-tile-badge-video">Video</span>';
985
+ }
986
+ else if (tmpCategory === 'audio')
987
+ {
988
+ tmpHTML += '<div class="retold-remote-tile-thumb-icon"><span class="retold-remote-icon retold-remote-icon-md">' + (tmpIconProvider ? tmpIconProvider.getIcon('file-audio', 48) : '') + '</span></div>';
989
+ tmpHTML += '<span class="retold-remote-tile-badge retold-remote-tile-badge-audio">Audio</span>';
990
+ }
991
+ else if (tmpCategory === 'document')
992
+ {
993
+ tmpHTML += '<div class="retold-remote-tile-thumb-icon"><span class="retold-remote-icon retold-remote-icon-md">' + (tmpIconProvider ? tmpIconProvider.getIconForEntry(tmpItem, 48) : '') + '</span></div>';
994
+ tmpHTML += '<span class="retold-remote-tile-badge retold-remote-tile-badge-document">' + tmpExtension + '</span>';
995
+ }
996
+ else
997
+ {
998
+ tmpHTML += '<div class="retold-remote-tile-thumb-icon"><span class="retold-remote-icon retold-remote-icon-md">' + (tmpIconProvider ? tmpIconProvider.getIconForEntry(tmpItem, 48) : '') + '</span></div>';
999
+ }
1000
+
1001
+ tmpHTML += '</div>'; // end thumb
1002
+
1003
+ // Label
1004
+ tmpHTML += '<div class="retold-remote-tile-label" title="' + this._escapeHTML(tmpItem.Name) + '">' + this._escapeHTML(tmpItem.Name) + '</div>';
1005
+
1006
+ // Meta
1007
+ if (tmpItem.Type === 'file' && tmpItem.Size !== undefined)
1008
+ {
1009
+ tmpHTML += '<div class="retold-remote-tile-meta">' + this._formatFileSize(tmpItem.Size) + '</div>';
1010
+ }
1011
+ else if (tmpItem.Type === 'folder')
1012
+ {
1013
+ tmpHTML += '<div class="retold-remote-tile-meta">Folder</div>';
1014
+ }
1015
+
1016
+ tmpHTML += '</div>'; // end tile
1017
+ }
1018
+
1019
+ tmpHTML += '</div>'; // end grid
1020
+
1021
+ return tmpHTML;
1022
+ }
1023
+
1024
+ /**
1025
+ * Build the list view HTML.
1026
+ */
1027
+ _buildListHTML(pItems, pCursorIndex)
1028
+ {
1029
+ let tmpHTML = '<div class="retold-remote-list">';
1030
+ let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
1031
+
1032
+ for (let i = 0; i < pItems.length; i++)
1033
+ {
1034
+ let tmpItem = pItems[i];
1035
+ let tmpSelectedClass = (i === pCursorIndex) ? ' selected' : '';
1036
+ let tmpIcon = '';
1037
+ if (tmpIconProvider)
1038
+ {
1039
+ tmpIcon = '<span class="retold-remote-icon retold-remote-icon-sm">' + tmpIconProvider.getIconForEntry(tmpItem, 16) + '</span>';
1040
+ }
1041
+
1042
+ tmpHTML += '<div class="retold-remote-list-row' + tmpSelectedClass + '" '
1043
+ + 'data-index="' + i + '" '
1044
+ + 'onclick="pict.views[\'RetoldRemote-Gallery\'].onTileClick(' + i + ')" '
1045
+ + 'ondblclick="pict.views[\'RetoldRemote-Gallery\'].onTileDoubleClick(' + i + ')">';
1046
+
1047
+ tmpHTML += '<div class="retold-remote-list-icon">' + tmpIcon + '</div>';
1048
+ tmpHTML += '<div class="retold-remote-list-name">' + this._escapeHTML(tmpItem.Name) + '</div>';
1049
+
1050
+ if (tmpItem.Type === 'file' && tmpItem.Size !== undefined)
1051
+ {
1052
+ tmpHTML += '<div class="retold-remote-list-size">' + this._formatFileSize(tmpItem.Size) + '</div>';
1053
+ }
1054
+ else
1055
+ {
1056
+ tmpHTML += '<div class="retold-remote-list-size"></div>';
1057
+ }
1058
+
1059
+ if (tmpItem.Modified)
1060
+ {
1061
+ tmpHTML += '<div class="retold-remote-list-date">' + new Date(tmpItem.Modified).toLocaleDateString() + '</div>';
1062
+ }
1063
+ else
1064
+ {
1065
+ tmpHTML += '<div class="retold-remote-list-date"></div>';
1066
+ }
1067
+
1068
+ tmpHTML += '</div>';
1069
+ }
1070
+
1071
+ tmpHTML += '</div>';
1072
+
1073
+ return tmpHTML;
1074
+ }
1075
+
1076
+ // ──────────────────────────────────────────────
1077
+ // Lazy loading
1078
+ // ──────────────────────────────────────────────
1079
+
1080
+ /**
1081
+ * Set up IntersectionObserver for lazy-loading thumbnail images.
1082
+ */
1083
+ _setupLazyLoading()
1084
+ {
1085
+ if (this._intersectionObserver)
1086
+ {
1087
+ this._intersectionObserver.disconnect();
1088
+ }
1089
+
1090
+ let tmpImages = document.querySelectorAll('.retold-remote-tile-thumb img[data-src]');
1091
+ if (tmpImages.length === 0)
1092
+ {
1093
+ return;
1094
+ }
1095
+
1096
+ this._intersectionObserver = new IntersectionObserver(
1097
+ (pEntries) =>
1098
+ {
1099
+ for (let i = 0; i < pEntries.length; i++)
1100
+ {
1101
+ if (pEntries[i].isIntersecting)
1102
+ {
1103
+ let tmpImg = pEntries[i].target;
1104
+ tmpImg.src = tmpImg.getAttribute('data-src');
1105
+ tmpImg.removeAttribute('data-src');
1106
+ this._intersectionObserver.unobserve(tmpImg);
1107
+ }
1108
+ }
1109
+ },
1110
+ { rootMargin: '200px' }
1111
+ );
1112
+
1113
+ for (let i = 0; i < tmpImages.length; i++)
1114
+ {
1115
+ this._intersectionObserver.observe(tmpImages[i]);
1116
+ }
1117
+ }
1118
+
1119
+ // ──────────────────────────────────────────────
1120
+ // Event handlers
1121
+ // ──────────────────────────────────────────────
1122
+
1123
+ /**
1124
+ * Handle single click on a tile (select it).
1125
+ */
1126
+ onTileClick(pIndex)
1127
+ {
1128
+ let tmpNavProvider = this.pict.providers['RetoldRemote-GalleryNavigation'];
1129
+ if (tmpNavProvider)
1130
+ {
1131
+ tmpNavProvider.moveCursor(pIndex);
1132
+ }
1133
+ }
1134
+
1135
+ /**
1136
+ * Handle double click on a tile (open it).
1137
+ */
1138
+ onTileDoubleClick(pIndex)
1139
+ {
1140
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1141
+ tmpRemote.GalleryCursorIndex = pIndex;
1142
+
1143
+ let tmpNavProvider = this.pict.providers['RetoldRemote-GalleryNavigation'];
1144
+ if (tmpNavProvider)
1145
+ {
1146
+ tmpNavProvider.openCurrent();
1147
+ }
1148
+ }
1149
+
1150
+ /**
1151
+ * Set the media type filter (via type filter buttons).
1152
+ */
1153
+ setFilter(pFilter)
1154
+ {
1155
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1156
+ tmpRemote.GalleryFilter = pFilter;
1157
+ tmpRemote.FilterState.MediaType = pFilter;
1158
+
1159
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1160
+ if (tmpFilterSort)
1161
+ {
1162
+ tmpFilterSort.applyFilterSort();
1163
+ }
1164
+ }
1165
+
1166
+ /**
1167
+ * Handle search input.
1168
+ */
1169
+ onSearchInput(pValue)
1170
+ {
1171
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1172
+ tmpRemote.SearchQuery = (pValue || '').toLowerCase();
1173
+
1174
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1175
+ if (tmpFilterSort)
1176
+ {
1177
+ tmpFilterSort.applyFilterSort();
1178
+ }
1179
+ }
1180
+
1181
+ /**
1182
+ * Handle sort dropdown change.
1183
+ */
1184
+ onSortChange(pValue)
1185
+ {
1186
+ let tmpParts = pValue.split(':');
1187
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1188
+ tmpRemote.SortField = tmpParts[0];
1189
+ tmpRemote.SortDirection = tmpParts[1] || 'asc';
1190
+
1191
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1192
+ if (tmpFilterSort)
1193
+ {
1194
+ tmpFilterSort.applyFilterSort();
1195
+ }
1196
+
1197
+ // Persist sort preference
1198
+ if (this.pict.PictApplication && this.pict.PictApplication.saveSettings)
1199
+ {
1200
+ this.pict.PictApplication.saveSettings();
1201
+ }
1202
+ }
1203
+
1204
+ /**
1205
+ * Toggle the advanced filter panel.
1206
+ */
1207
+ toggleFilterPanel()
1208
+ {
1209
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1210
+ tmpRemote.FilterPanelOpen = !tmpRemote.FilterPanelOpen;
1211
+ this.renderGallery();
1212
+
1213
+ if (this.pict.PictApplication && this.pict.PictApplication.saveSettings)
1214
+ {
1215
+ this.pict.PictApplication.saveSettings();
1216
+ }
1217
+ }
1218
+
1219
+ /**
1220
+ * Handle extension checkbox toggle.
1221
+ */
1222
+ onExtensionToggle(pExtension, pChecked)
1223
+ {
1224
+ let tmpFilterState = this.pict.AppData.RetoldRemote.FilterState;
1225
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1226
+
1227
+ if (pChecked)
1228
+ {
1229
+ if (tmpFilterState.Extensions.length > 0)
1230
+ {
1231
+ tmpFilterState.Extensions.push(pExtension);
1232
+ }
1233
+ // If empty (all selected), adding back is a no-op
1234
+ }
1235
+ else
1236
+ {
1237
+ // If removing from "all", populate the full list minus this one
1238
+ if (tmpFilterState.Extensions.length === 0 && tmpFilterSort)
1239
+ {
1240
+ let tmpAll = tmpFilterSort.getAvailableExtensions();
1241
+ tmpFilterState.Extensions = tmpAll.map((e) => e.ext).filter((e) => e !== pExtension);
1242
+ }
1243
+ else
1244
+ {
1245
+ tmpFilterState.Extensions = tmpFilterState.Extensions.filter((e) => e !== pExtension);
1246
+ }
1247
+ }
1248
+
1249
+ if (tmpFilterSort)
1250
+ {
1251
+ tmpFilterSort.applyFilterSort();
1252
+ }
1253
+ }
1254
+
1255
+ /**
1256
+ * Handle size filter change.
1257
+ */
1258
+ onSizeFilterChange(pWhich, pValueKB)
1259
+ {
1260
+ let tmpFilterState = this.pict.AppData.RetoldRemote.FilterState;
1261
+ let tmpBytes = (pValueKB && pValueKB !== '') ? parseInt(pValueKB, 10) * 1024 : null;
1262
+
1263
+ if (pWhich === 'min')
1264
+ {
1265
+ tmpFilterState.SizeMin = tmpBytes;
1266
+ }
1267
+ else
1268
+ {
1269
+ tmpFilterState.SizeMax = tmpBytes;
1270
+ }
1271
+
1272
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1273
+ if (tmpFilterSort)
1274
+ {
1275
+ tmpFilterSort.applyFilterSort();
1276
+ }
1277
+ }
1278
+
1279
+ /**
1280
+ * Handle date filter change.
1281
+ */
1282
+ onDateFilterChange(pField, pValue)
1283
+ {
1284
+ let tmpFilterState = this.pict.AppData.RetoldRemote.FilterState;
1285
+ tmpFilterState[pField] = pValue || null;
1286
+
1287
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1288
+ if (tmpFilterSort)
1289
+ {
1290
+ tmpFilterSort.applyFilterSort();
1291
+ }
1292
+ }
1293
+
1294
+ /**
1295
+ * Remove a filter chip.
1296
+ */
1297
+ removeFilterChip(pKey)
1298
+ {
1299
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1300
+ if (tmpFilterSort)
1301
+ {
1302
+ tmpFilterSort.removeFilter(pKey);
1303
+ tmpFilterSort.applyFilterSort();
1304
+ }
1305
+ }
1306
+
1307
+ /**
1308
+ * Clear all filters.
1309
+ */
1310
+ clearAllFilters()
1311
+ {
1312
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1313
+ if (tmpFilterSort)
1314
+ {
1315
+ tmpFilterSort.clearAllFilters();
1316
+ tmpFilterSort.applyFilterSort();
1317
+ }
1318
+ }
1319
+
1320
+ /**
1321
+ * Save the current filter config as a named preset.
1322
+ */
1323
+ saveFilterPreset()
1324
+ {
1325
+ let tmpInput = document.getElementById('RetoldRemote-Filter-PresetName');
1326
+ if (!tmpInput || !tmpInput.value.trim())
1327
+ {
1328
+ return;
1329
+ }
1330
+
1331
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1332
+ if (tmpFilterSort)
1333
+ {
1334
+ tmpFilterSort.savePreset(tmpInput.value.trim());
1335
+ }
1336
+
1337
+ if (this.pict.PictApplication && this.pict.PictApplication.saveSettings)
1338
+ {
1339
+ this.pict.PictApplication.saveSettings();
1340
+ }
1341
+
1342
+ this.renderGallery();
1343
+ }
1344
+
1345
+ /**
1346
+ * Load a filter preset from the dropdown.
1347
+ */
1348
+ loadFilterPreset(pIndex)
1349
+ {
1350
+ if (pIndex === '' || pIndex === null || pIndex === undefined)
1351
+ {
1352
+ return;
1353
+ }
1354
+
1355
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1356
+ if (tmpFilterSort)
1357
+ {
1358
+ tmpFilterSort.loadPreset(parseInt(pIndex, 10));
1359
+ tmpFilterSort.applyFilterSort();
1360
+ }
1361
+
1362
+ if (this.pict.PictApplication && this.pict.PictApplication.saveSettings)
1363
+ {
1364
+ this.pict.PictApplication.saveSettings();
1365
+ }
1366
+ }
1367
+
1368
+ /**
1369
+ * Delete the currently selected preset from the dropdown.
1370
+ */
1371
+ deleteSelectedPreset()
1372
+ {
1373
+ let tmpSelect = document.getElementById('RetoldRemote-Filter-PresetSelect');
1374
+ if (!tmpSelect || tmpSelect.value === '')
1375
+ {
1376
+ return;
1377
+ }
1378
+
1379
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1380
+ if (tmpFilterSort)
1381
+ {
1382
+ tmpFilterSort.deletePreset(parseInt(tmpSelect.value, 10));
1383
+ }
1384
+
1385
+ if (this.pict.PictApplication && this.pict.PictApplication.saveSettings)
1386
+ {
1387
+ this.pict.PictApplication.saveSettings();
1388
+ }
1389
+
1390
+ this.renderGallery();
1391
+ }
1392
+
1393
+ // ──────────────────────────────────────────────
1394
+ // Utilities
1395
+ // ──────────────────────────────────────────────
1396
+
1397
+ /**
1398
+ * Get the media category for a file.
1399
+ */
1400
+ _getCategory(pExtension, pType)
1401
+ {
1402
+ if (pType === 'folder') return 'folder';
1403
+ // Delegate to the filter/sort provider if available
1404
+ let tmpFilterSort = this.pict.providers['RetoldRemote-GalleryFilterSort'];
1405
+ if (tmpFilterSort)
1406
+ {
1407
+ return tmpFilterSort.getCategory(pExtension);
1408
+ }
1409
+ // Fallback
1410
+ let tmpExt = (pExtension || '').replace(/^\./, '').toLowerCase();
1411
+ if (tmpExt === 'png' || tmpExt === 'jpg' || tmpExt === 'jpeg' || tmpExt === 'gif' || tmpExt === 'webp') return 'image';
1412
+ if (tmpExt === 'mp4' || tmpExt === 'webm' || tmpExt === 'mov') return 'video';
1413
+ if (tmpExt === 'mp3' || tmpExt === 'wav' || tmpExt === 'ogg') return 'audio';
1414
+ if (tmpExt === 'pdf') return 'document';
1415
+ return 'other';
1416
+ }
1417
+
1418
+ _formatFileSize(pBytes)
1419
+ {
1420
+ if (!pBytes || pBytes === 0) return '0 B';
1421
+ let tmpUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
1422
+ let tmpIndex = Math.floor(Math.log(pBytes) / Math.log(1024));
1423
+ if (tmpIndex >= tmpUnits.length) tmpIndex = tmpUnits.length - 1;
1424
+ let tmpSize = pBytes / Math.pow(1024, tmpIndex);
1425
+ return tmpSize.toFixed(tmpIndex === 0 ? 0 : 1) + ' ' + tmpUnits[tmpIndex];
1426
+ }
1427
+
1428
+ _escapeHTML(pText)
1429
+ {
1430
+ if (!pText) return '';
1431
+ return pText.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1432
+ }
1433
+ }
1434
+
1435
+ RetoldRemoteGalleryView.default_configuration = _ViewConfiguration;
1436
+
1437
+ module.exports = RetoldRemoteGalleryView;