retold-remote 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/retold-remote.css +3 -0
- package/html/index.html +1 -1
- package/package.json +1 -1
- package/source/Pict-Application-RetoldRemote.js +21 -2
- package/source/cli/RetoldRemote-Server-Setup.js +129 -0
- package/source/providers/Pict-Provider-AISortManager.js +456 -0
- package/source/providers/Pict-Provider-CollectionManager.js +266 -0
- package/source/server/RetoldRemote-AISortService.js +879 -0
- package/source/server/RetoldRemote-CollectionService.js +161 -2
- package/source/server/RetoldRemote-FileOperationService.js +560 -0
- package/source/server/RetoldRemote-MediaService.js +12 -0
- package/source/server/RetoldRemote-MetadataCache.js +411 -0
- package/source/views/PictView-Remote-CollectionsPanel.js +435 -36
- package/source/views/PictView-Remote-Layout.js +2 -0
- package/source/views/PictView-Remote-SettingsPanel.js +156 -0
- package/source/views/PictView-Remote-TopBar.js +86 -0
- package/web-application/css/retold-remote.css +3 -0
- package/web-application/index.html +1 -1
- package/web-application/retold-remote.js +402 -34
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +32 -15
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -285,6 +285,155 @@ const _ViewConfiguration =
|
|
|
285
285
|
color: var(--retold-danger-muted, #e55);
|
|
286
286
|
background: rgba(200, 50, 50, 0.1);
|
|
287
287
|
}
|
|
288
|
+
/* ---- Operation Plan mode ---- */
|
|
289
|
+
.retold-remote-collections-op-controls
|
|
290
|
+
{
|
|
291
|
+
flex-direction: column;
|
|
292
|
+
gap: 6px;
|
|
293
|
+
}
|
|
294
|
+
.retold-remote-collections-op-summary
|
|
295
|
+
{
|
|
296
|
+
font-size: 0.75rem;
|
|
297
|
+
color: var(--retold-text-dim);
|
|
298
|
+
padding: 4px 0;
|
|
299
|
+
}
|
|
300
|
+
.retold-remote-collections-op-buttons
|
|
301
|
+
{
|
|
302
|
+
display: flex;
|
|
303
|
+
gap: 6px;
|
|
304
|
+
}
|
|
305
|
+
.retold-remote-collections-op-execute-btn
|
|
306
|
+
{
|
|
307
|
+
flex: 1;
|
|
308
|
+
padding: 6px 12px;
|
|
309
|
+
border: none;
|
|
310
|
+
border-radius: 3px;
|
|
311
|
+
background: var(--retold-accent, #4a90d9);
|
|
312
|
+
color: #fff;
|
|
313
|
+
font-size: 0.78rem;
|
|
314
|
+
font-weight: 600;
|
|
315
|
+
cursor: pointer;
|
|
316
|
+
}
|
|
317
|
+
.retold-remote-collections-op-execute-btn:hover
|
|
318
|
+
{
|
|
319
|
+
opacity: 0.9;
|
|
320
|
+
}
|
|
321
|
+
.retold-remote-collections-op-execute-btn:disabled
|
|
322
|
+
{
|
|
323
|
+
opacity: 0.5;
|
|
324
|
+
cursor: default;
|
|
325
|
+
}
|
|
326
|
+
.retold-remote-collections-op-undo-btn
|
|
327
|
+
{
|
|
328
|
+
padding: 6px 12px;
|
|
329
|
+
border: 1px solid var(--retold-border);
|
|
330
|
+
border-radius: 3px;
|
|
331
|
+
background: transparent;
|
|
332
|
+
color: var(--retold-text-secondary);
|
|
333
|
+
font-size: 0.78rem;
|
|
334
|
+
cursor: pointer;
|
|
335
|
+
}
|
|
336
|
+
.retold-remote-collections-op-undo-btn:hover
|
|
337
|
+
{
|
|
338
|
+
background: var(--retold-bg-tertiary);
|
|
339
|
+
}
|
|
340
|
+
.retold-remote-collection-op-item
|
|
341
|
+
{
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
gap: 4px;
|
|
345
|
+
padding: 6px 8px;
|
|
346
|
+
border-bottom: 1px solid var(--retold-border);
|
|
347
|
+
font-size: 0.75rem;
|
|
348
|
+
flex-wrap: wrap;
|
|
349
|
+
}
|
|
350
|
+
.retold-remote-collection-op-item.op-status-completed
|
|
351
|
+
{
|
|
352
|
+
opacity: 0.6;
|
|
353
|
+
}
|
|
354
|
+
.retold-remote-collection-op-item.op-status-skipped
|
|
355
|
+
{
|
|
356
|
+
opacity: 0.4;
|
|
357
|
+
text-decoration: line-through;
|
|
358
|
+
}
|
|
359
|
+
.retold-remote-collection-op-item.op-status-failed
|
|
360
|
+
{
|
|
361
|
+
background: rgba(200, 50, 50, 0.05);
|
|
362
|
+
}
|
|
363
|
+
.retold-remote-collection-op-status
|
|
364
|
+
{
|
|
365
|
+
flex-shrink: 0;
|
|
366
|
+
width: 16px;
|
|
367
|
+
text-align: center;
|
|
368
|
+
font-size: 0.8rem;
|
|
369
|
+
}
|
|
370
|
+
.op-status-completed .retold-remote-collection-op-status
|
|
371
|
+
{
|
|
372
|
+
color: var(--retold-success, #4a4);
|
|
373
|
+
}
|
|
374
|
+
.op-status-failed .retold-remote-collection-op-status
|
|
375
|
+
{
|
|
376
|
+
color: var(--retold-danger-muted, #e55);
|
|
377
|
+
}
|
|
378
|
+
.op-status-pending .retold-remote-collection-op-status
|
|
379
|
+
{
|
|
380
|
+
color: var(--retold-text-dim);
|
|
381
|
+
}
|
|
382
|
+
.retold-remote-collection-op-source
|
|
383
|
+
{
|
|
384
|
+
flex: 1;
|
|
385
|
+
min-width: 0;
|
|
386
|
+
overflow: hidden;
|
|
387
|
+
text-overflow: ellipsis;
|
|
388
|
+
white-space: nowrap;
|
|
389
|
+
color: var(--retold-text-dim);
|
|
390
|
+
}
|
|
391
|
+
.retold-remote-collection-op-arrow
|
|
392
|
+
{
|
|
393
|
+
flex-shrink: 0;
|
|
394
|
+
color: var(--retold-text-dim);
|
|
395
|
+
padding: 0 2px;
|
|
396
|
+
}
|
|
397
|
+
.retold-remote-collection-op-dest
|
|
398
|
+
{
|
|
399
|
+
flex: 2;
|
|
400
|
+
min-width: 0;
|
|
401
|
+
overflow: hidden;
|
|
402
|
+
text-overflow: ellipsis;
|
|
403
|
+
white-space: nowrap;
|
|
404
|
+
color: var(--retold-text-secondary);
|
|
405
|
+
}
|
|
406
|
+
.retold-remote-collection-op-dest-input
|
|
407
|
+
{
|
|
408
|
+
width: 100%;
|
|
409
|
+
padding: 2px 4px;
|
|
410
|
+
border: 1px solid var(--retold-accent);
|
|
411
|
+
border-radius: 2px;
|
|
412
|
+
background: var(--retold-bg-tertiary);
|
|
413
|
+
color: var(--retold-text-secondary);
|
|
414
|
+
font-size: 0.75rem;
|
|
415
|
+
font-family: inherit;
|
|
416
|
+
box-sizing: border-box;
|
|
417
|
+
}
|
|
418
|
+
.retold-remote-collection-op-badge
|
|
419
|
+
{
|
|
420
|
+
flex-shrink: 0;
|
|
421
|
+
font-size: 0.6rem;
|
|
422
|
+
padding: 1px 4px;
|
|
423
|
+
border-radius: 2px;
|
|
424
|
+
background: var(--retold-bg-tertiary);
|
|
425
|
+
color: var(--retold-text-dim);
|
|
426
|
+
text-transform: uppercase;
|
|
427
|
+
font-weight: 600;
|
|
428
|
+
letter-spacing: 0.3px;
|
|
429
|
+
}
|
|
430
|
+
.retold-remote-collection-op-error
|
|
431
|
+
{
|
|
432
|
+
width: 100%;
|
|
433
|
+
font-size: 0.68rem;
|
|
434
|
+
color: var(--retold-danger-muted, #e55);
|
|
435
|
+
padding: 2px 0 0 20px;
|
|
436
|
+
}
|
|
288
437
|
/* ---- Edit mode ---- */
|
|
289
438
|
.retold-remote-collections-edit
|
|
290
439
|
{
|
|
@@ -644,57 +793,135 @@ class RetoldRemoteCollectionsPanelView extends libPictView
|
|
|
644
793
|
tmpHeader.appendChild(tmpEditBtn);
|
|
645
794
|
pRoot.appendChild(tmpHeader);
|
|
646
795
|
|
|
647
|
-
//
|
|
648
|
-
let
|
|
649
|
-
|
|
796
|
+
// Check if this is an operation-plan collection
|
|
797
|
+
let tmpIsOperationPlan = (tmpCollection.CollectionType === 'operation-plan');
|
|
798
|
+
|
|
799
|
+
if (tmpIsOperationPlan)
|
|
800
|
+
{
|
|
801
|
+
// Operation plan controls: summary + execute/undo buttons
|
|
802
|
+
let tmpOpControls = document.createElement('div');
|
|
803
|
+
tmpOpControls.className = 'retold-remote-collections-detail-controls retold-remote-collections-op-controls';
|
|
650
804
|
|
|
651
|
-
|
|
652
|
-
|
|
805
|
+
// Count pending, completed, failed, skipped
|
|
806
|
+
let tmpItems = tmpCollection.Items || [];
|
|
807
|
+
let tmpPending = 0;
|
|
808
|
+
let tmpCompleted = 0;
|
|
809
|
+
let tmpFailed = 0;
|
|
810
|
+
let tmpSkipped = 0;
|
|
811
|
+
for (let i = 0; i < tmpItems.length; i++)
|
|
812
|
+
{
|
|
813
|
+
let tmpStatus = tmpItems[i].OperationStatus;
|
|
814
|
+
if (tmpStatus === 'completed') tmpCompleted++;
|
|
815
|
+
else if (tmpStatus === 'failed') tmpFailed++;
|
|
816
|
+
else if (tmpStatus === 'skipped') tmpSkipped++;
|
|
817
|
+
else if (tmpItems[i].Operation) tmpPending++;
|
|
818
|
+
}
|
|
653
819
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
820
|
+
let tmpSummary = document.createElement('div');
|
|
821
|
+
tmpSummary.className = 'retold-remote-collections-op-summary';
|
|
822
|
+
let tmpSummaryParts = [];
|
|
823
|
+
if (tmpPending > 0) tmpSummaryParts.push(tmpPending + ' pending');
|
|
824
|
+
if (tmpCompleted > 0) tmpSummaryParts.push(tmpCompleted + ' done');
|
|
825
|
+
if (tmpFailed > 0) tmpSummaryParts.push(tmpFailed + ' failed');
|
|
826
|
+
if (tmpSkipped > 0) tmpSummaryParts.push(tmpSkipped + ' skipped');
|
|
827
|
+
tmpSummary.textContent = tmpSummaryParts.join(' \u00b7 ') || 'No operations';
|
|
828
|
+
tmpOpControls.appendChild(tmpSummary);
|
|
660
829
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
tmpOpt.textContent = tmpSortOptions[i].label;
|
|
666
|
-
if (tmpCollection.SortMode === tmpSortOptions[i].value)
|
|
830
|
+
let tmpBtnRow = document.createElement('div');
|
|
831
|
+
tmpBtnRow.className = 'retold-remote-collections-op-buttons';
|
|
832
|
+
|
|
833
|
+
if (tmpPending > 0)
|
|
667
834
|
{
|
|
668
|
-
|
|
835
|
+
let tmpExecBtn = document.createElement('button');
|
|
836
|
+
tmpExecBtn.className = 'retold-remote-collections-op-execute-btn';
|
|
837
|
+
tmpExecBtn.textContent = 'Execute ' + tmpPending + ' Move' + (tmpPending > 1 ? 's' : '');
|
|
838
|
+
tmpExecBtn.onclick = () =>
|
|
839
|
+
{
|
|
840
|
+
tmpExecBtn.disabled = true;
|
|
841
|
+
tmpExecBtn.textContent = 'Moving...';
|
|
842
|
+
tmpManager.executeCollectionOperations(tmpCollection.GUID);
|
|
843
|
+
};
|
|
844
|
+
tmpBtnRow.appendChild(tmpExecBtn);
|
|
669
845
|
}
|
|
670
|
-
tmpSortSelect.appendChild(tmpOpt);
|
|
671
|
-
}
|
|
672
846
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
847
|
+
if (tmpCollection.OperationBatchGUID && tmpCompleted > 0)
|
|
848
|
+
{
|
|
849
|
+
let tmpUndoBtn = document.createElement('button');
|
|
850
|
+
tmpUndoBtn.className = 'retold-remote-collections-op-undo-btn';
|
|
851
|
+
tmpUndoBtn.textContent = 'Undo';
|
|
852
|
+
tmpUndoBtn.onclick = () =>
|
|
853
|
+
{
|
|
854
|
+
tmpUndoBtn.disabled = true;
|
|
855
|
+
tmpUndoBtn.textContent = 'Undoing...';
|
|
856
|
+
tmpManager.undoCollectionOperations(tmpCollection.GUID);
|
|
857
|
+
};
|
|
858
|
+
tmpBtnRow.appendChild(tmpUndoBtn);
|
|
859
|
+
}
|
|
677
860
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
tmpDirBtn.onclick = () =>
|
|
861
|
+
tmpOpControls.appendChild(tmpBtnRow);
|
|
862
|
+
pRoot.appendChild(tmpOpControls);
|
|
863
|
+
}
|
|
864
|
+
else
|
|
683
865
|
{
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
866
|
+
// Standard sort controls for bookmark collections
|
|
867
|
+
let tmpControls = document.createElement('div');
|
|
868
|
+
tmpControls.className = 'retold-remote-collections-detail-controls';
|
|
869
|
+
|
|
870
|
+
let tmpSortSelect = document.createElement('select');
|
|
871
|
+
tmpSortSelect.className = 'retold-remote-collections-sort-select';
|
|
687
872
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
873
|
+
let tmpSortOptions = [
|
|
874
|
+
{ value: 'manual', label: 'Manual' },
|
|
875
|
+
{ value: 'name', label: 'Name' },
|
|
876
|
+
{ value: 'modified', label: 'Date Added' },
|
|
877
|
+
{ value: 'type', label: 'Type' }
|
|
878
|
+
];
|
|
879
|
+
|
|
880
|
+
for (let i = 0; i < tmpSortOptions.length; i++)
|
|
881
|
+
{
|
|
882
|
+
let tmpOpt = document.createElement('option');
|
|
883
|
+
tmpOpt.value = tmpSortOptions[i].value;
|
|
884
|
+
tmpOpt.textContent = tmpSortOptions[i].label;
|
|
885
|
+
if (tmpCollection.SortMode === tmpSortOptions[i].value)
|
|
886
|
+
{
|
|
887
|
+
tmpOpt.selected = true;
|
|
888
|
+
}
|
|
889
|
+
tmpSortSelect.appendChild(tmpOpt);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
tmpSortSelect.onchange = (pEvent) =>
|
|
893
|
+
{
|
|
894
|
+
tmpManager.sortActiveCollection(pEvent.target.value, null);
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
let tmpDirBtn = document.createElement('button');
|
|
898
|
+
tmpDirBtn.className = 'retold-remote-collections-sort-dir';
|
|
899
|
+
tmpDirBtn.textContent = tmpCollection.SortDirection === 'desc' ? '\u2193' : '\u2191';
|
|
900
|
+
tmpDirBtn.title = tmpCollection.SortDirection === 'desc' ? 'Descending' : 'Ascending';
|
|
901
|
+
tmpDirBtn.onclick = () =>
|
|
902
|
+
{
|
|
903
|
+
let tmpNewDir = (tmpRemote.ActiveCollection.SortDirection === 'desc') ? 'asc' : 'desc';
|
|
904
|
+
tmpManager.sortActiveCollection(null, tmpNewDir);
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
tmpControls.appendChild(tmpSortSelect);
|
|
908
|
+
tmpControls.appendChild(tmpDirBtn);
|
|
909
|
+
pRoot.appendChild(tmpControls);
|
|
910
|
+
}
|
|
691
911
|
|
|
692
912
|
// Item list
|
|
693
913
|
let tmpBody = document.createElement('div');
|
|
694
914
|
tmpBody.className = 'retold-remote-collections-body';
|
|
695
915
|
pRoot.appendChild(tmpBody);
|
|
696
916
|
|
|
697
|
-
|
|
917
|
+
if (tmpIsOperationPlan)
|
|
918
|
+
{
|
|
919
|
+
this._renderOperationItemList(tmpBody, tmpCollection);
|
|
920
|
+
}
|
|
921
|
+
else
|
|
922
|
+
{
|
|
923
|
+
this._renderItemList(tmpBody, tmpCollection);
|
|
924
|
+
}
|
|
698
925
|
}
|
|
699
926
|
|
|
700
927
|
_renderItemList(pBody, pCollection)
|
|
@@ -862,6 +1089,178 @@ class RetoldRemoteCollectionsPanelView extends libPictView
|
|
|
862
1089
|
}
|
|
863
1090
|
}
|
|
864
1091
|
|
|
1092
|
+
// -- Operation Plan Item Rendering ------------------------------------
|
|
1093
|
+
|
|
1094
|
+
_renderOperationItemList(pBody, pCollection)
|
|
1095
|
+
{
|
|
1096
|
+
let tmpSelf = this;
|
|
1097
|
+
let tmpManager = this.pict.providers['RetoldRemote-CollectionManager'];
|
|
1098
|
+
let tmpItems = pCollection.Items || [];
|
|
1099
|
+
|
|
1100
|
+
pBody.innerHTML = '';
|
|
1101
|
+
|
|
1102
|
+
if (tmpItems.length === 0)
|
|
1103
|
+
{
|
|
1104
|
+
let tmpEmpty = document.createElement('div');
|
|
1105
|
+
tmpEmpty.className = 'retold-remote-collections-empty';
|
|
1106
|
+
tmpEmpty.textContent = 'No items in this sort plan.';
|
|
1107
|
+
pBody.appendChild(tmpEmpty);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
for (let i = 0; i < tmpItems.length; i++)
|
|
1112
|
+
{
|
|
1113
|
+
let tmpItem = tmpItems[i];
|
|
1114
|
+
let tmpRow = document.createElement('div');
|
|
1115
|
+
tmpRow.className = 'retold-remote-collection-op-item';
|
|
1116
|
+
|
|
1117
|
+
// Status color
|
|
1118
|
+
let tmpStatus = tmpItem.OperationStatus || 'pending';
|
|
1119
|
+
tmpRow.classList.add('op-status-' + tmpStatus);
|
|
1120
|
+
|
|
1121
|
+
// Status indicator
|
|
1122
|
+
let tmpStatusDiv = document.createElement('div');
|
|
1123
|
+
tmpStatusDiv.className = 'retold-remote-collection-op-status';
|
|
1124
|
+
if (tmpStatus === 'completed')
|
|
1125
|
+
{
|
|
1126
|
+
tmpStatusDiv.textContent = '\u2713';
|
|
1127
|
+
tmpStatusDiv.title = 'Completed';
|
|
1128
|
+
}
|
|
1129
|
+
else if (tmpStatus === 'failed')
|
|
1130
|
+
{
|
|
1131
|
+
tmpStatusDiv.textContent = '\u2717';
|
|
1132
|
+
tmpStatusDiv.title = tmpItem.OperationError || 'Failed';
|
|
1133
|
+
}
|
|
1134
|
+
else if (tmpStatus === 'skipped')
|
|
1135
|
+
{
|
|
1136
|
+
tmpStatusDiv.textContent = '\u2014';
|
|
1137
|
+
tmpStatusDiv.title = 'Skipped';
|
|
1138
|
+
}
|
|
1139
|
+
else
|
|
1140
|
+
{
|
|
1141
|
+
tmpStatusDiv.textContent = '\u25CB';
|
|
1142
|
+
tmpStatusDiv.title = 'Pending';
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Source path (filename only, full path in tooltip)
|
|
1146
|
+
let tmpSourceDiv = document.createElement('div');
|
|
1147
|
+
tmpSourceDiv.className = 'retold-remote-collection-op-source';
|
|
1148
|
+
let tmpSourcePath = tmpItem.Path || '';
|
|
1149
|
+
tmpSourceDiv.textContent = tmpSourcePath.split('/').pop() || tmpSourcePath;
|
|
1150
|
+
tmpSourceDiv.title = tmpSourcePath;
|
|
1151
|
+
|
|
1152
|
+
// Arrow
|
|
1153
|
+
let tmpArrow = document.createElement('div');
|
|
1154
|
+
tmpArrow.className = 'retold-remote-collection-op-arrow';
|
|
1155
|
+
tmpArrow.textContent = '\u2192';
|
|
1156
|
+
|
|
1157
|
+
// Destination path (editable)
|
|
1158
|
+
let tmpDestDiv = document.createElement('div');
|
|
1159
|
+
tmpDestDiv.className = 'retold-remote-collection-op-dest';
|
|
1160
|
+
let tmpDestPath = tmpItem.DestinationPath || '';
|
|
1161
|
+
tmpDestDiv.textContent = tmpDestPath || '(no destination)';
|
|
1162
|
+
tmpDestDiv.title = tmpDestPath;
|
|
1163
|
+
|
|
1164
|
+
// Make destination editable on click (only for pending items)
|
|
1165
|
+
if (tmpStatus === 'pending')
|
|
1166
|
+
{
|
|
1167
|
+
tmpDestDiv.style.cursor = 'pointer';
|
|
1168
|
+
tmpDestDiv.onclick = (pEvent) =>
|
|
1169
|
+
{
|
|
1170
|
+
pEvent.stopPropagation();
|
|
1171
|
+
tmpSelf._startEditDestination(tmpDestDiv, tmpItem, pCollection);
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Operation badge
|
|
1176
|
+
let tmpOpBadge = document.createElement('div');
|
|
1177
|
+
tmpOpBadge.className = 'retold-remote-collection-op-badge';
|
|
1178
|
+
tmpOpBadge.textContent = (tmpItem.Operation || 'move').toUpperCase();
|
|
1179
|
+
|
|
1180
|
+
// Skip/remove button (only for pending items)
|
|
1181
|
+
let tmpSkipBtn = document.createElement('button');
|
|
1182
|
+
tmpSkipBtn.className = 'retold-remote-collection-item-remove';
|
|
1183
|
+
tmpSkipBtn.title = 'Skip this operation';
|
|
1184
|
+
tmpSkipBtn.textContent = '\u00d7';
|
|
1185
|
+
|
|
1186
|
+
if (tmpStatus === 'pending')
|
|
1187
|
+
{
|
|
1188
|
+
tmpSkipBtn.onclick = (pEvent) =>
|
|
1189
|
+
{
|
|
1190
|
+
pEvent.stopPropagation();
|
|
1191
|
+
tmpManager.skipItemOperation(tmpItem.ID);
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
else
|
|
1195
|
+
{
|
|
1196
|
+
tmpSkipBtn.style.visibility = 'hidden';
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Error message (if failed)
|
|
1200
|
+
if (tmpStatus === 'failed' && tmpItem.OperationError)
|
|
1201
|
+
{
|
|
1202
|
+
let tmpErrorDiv = document.createElement('div');
|
|
1203
|
+
tmpErrorDiv.className = 'retold-remote-collection-op-error';
|
|
1204
|
+
tmpErrorDiv.textContent = tmpItem.OperationError;
|
|
1205
|
+
tmpRow.appendChild(tmpErrorDiv);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
tmpRow.appendChild(tmpStatusDiv);
|
|
1209
|
+
tmpRow.appendChild(tmpSourceDiv);
|
|
1210
|
+
tmpRow.appendChild(tmpArrow);
|
|
1211
|
+
tmpRow.appendChild(tmpDestDiv);
|
|
1212
|
+
tmpRow.appendChild(tmpOpBadge);
|
|
1213
|
+
tmpRow.appendChild(tmpSkipBtn);
|
|
1214
|
+
pBody.appendChild(tmpRow);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Start inline editing of an operation item's destination path.
|
|
1220
|
+
*/
|
|
1221
|
+
_startEditDestination(pDestDiv, pItem, pCollection)
|
|
1222
|
+
{
|
|
1223
|
+
let tmpSelf = this;
|
|
1224
|
+
let tmpManager = this.pict.providers['RetoldRemote-CollectionManager'];
|
|
1225
|
+
|
|
1226
|
+
let tmpInput = document.createElement('input');
|
|
1227
|
+
tmpInput.type = 'text';
|
|
1228
|
+
tmpInput.className = 'retold-remote-collection-op-dest-input';
|
|
1229
|
+
tmpInput.value = pItem.DestinationPath || '';
|
|
1230
|
+
|
|
1231
|
+
let tmpFinishEdit = () =>
|
|
1232
|
+
{
|
|
1233
|
+
let tmpNewDest = tmpInput.value.trim();
|
|
1234
|
+
if (tmpNewDest && tmpNewDest !== pItem.DestinationPath)
|
|
1235
|
+
{
|
|
1236
|
+
tmpManager.setItemDestination(pItem.ID, tmpNewDest);
|
|
1237
|
+
}
|
|
1238
|
+
pDestDiv.textContent = tmpNewDest || pItem.DestinationPath || '(no destination)';
|
|
1239
|
+
pDestDiv.title = tmpNewDest || pItem.DestinationPath || '';
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
tmpInput.onblur = tmpFinishEdit;
|
|
1243
|
+
tmpInput.onkeydown = (pEvent) =>
|
|
1244
|
+
{
|
|
1245
|
+
if (pEvent.key === 'Enter')
|
|
1246
|
+
{
|
|
1247
|
+
pEvent.preventDefault();
|
|
1248
|
+
tmpInput.blur();
|
|
1249
|
+
}
|
|
1250
|
+
else if (pEvent.key === 'Escape')
|
|
1251
|
+
{
|
|
1252
|
+
pEvent.preventDefault();
|
|
1253
|
+
pDestDiv.textContent = pItem.DestinationPath || '(no destination)';
|
|
1254
|
+
pDestDiv.title = pItem.DestinationPath || '';
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
pDestDiv.textContent = '';
|
|
1259
|
+
pDestDiv.appendChild(tmpInput);
|
|
1260
|
+
tmpInput.focus();
|
|
1261
|
+
tmpInput.select();
|
|
1262
|
+
}
|
|
1263
|
+
|
|
865
1264
|
// -- Edit Mode --------------------------------------------------------
|
|
866
1265
|
|
|
867
1266
|
_renderEditMode(pRoot)
|
|
@@ -15,6 +15,7 @@ const _ViewConfiguration =
|
|
|
15
15
|
display: flex;
|
|
16
16
|
flex-direction: column;
|
|
17
17
|
height: 100vh;
|
|
18
|
+
height: 100dvh;
|
|
18
19
|
background: var(--retold-bg-primary);
|
|
19
20
|
color: var(--retold-text-primary);
|
|
20
21
|
font-family: var(--retold-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif);
|
|
@@ -278,6 +279,7 @@ const _ViewConfiguration =
|
|
|
278
279
|
{
|
|
279
280
|
width: 100% !important;
|
|
280
281
|
height: 33vh;
|
|
282
|
+
height: 33dvh;
|
|
281
283
|
transition: height 0.2s ease;
|
|
282
284
|
flex-direction: column;
|
|
283
285
|
}
|
|
@@ -97,6 +97,42 @@ const _ViewConfiguration =
|
|
|
97
97
|
background: var(--retold-bg-hover);
|
|
98
98
|
color: var(--retold-text-primary);
|
|
99
99
|
}
|
|
100
|
+
.retold-remote-settings-input
|
|
101
|
+
{
|
|
102
|
+
width: 100%;
|
|
103
|
+
padding: 5px 8px;
|
|
104
|
+
border: 1px solid var(--retold-border);
|
|
105
|
+
border-radius: 3px;
|
|
106
|
+
background: var(--retold-bg-tertiary);
|
|
107
|
+
color: var(--retold-text-secondary);
|
|
108
|
+
font-size: 0.75rem;
|
|
109
|
+
font-family: inherit;
|
|
110
|
+
box-sizing: border-box;
|
|
111
|
+
}
|
|
112
|
+
.retold-remote-settings-input:focus
|
|
113
|
+
{
|
|
114
|
+
outline: none;
|
|
115
|
+
border-color: var(--retold-accent);
|
|
116
|
+
}
|
|
117
|
+
.retold-remote-settings-input-row
|
|
118
|
+
{
|
|
119
|
+
margin-bottom: 8px;
|
|
120
|
+
}
|
|
121
|
+
.retold-remote-settings-input-label
|
|
122
|
+
{
|
|
123
|
+
display: block;
|
|
124
|
+
font-size: 0.72rem;
|
|
125
|
+
color: var(--retold-text-dim);
|
|
126
|
+
margin-bottom: 3px;
|
|
127
|
+
}
|
|
128
|
+
.retold-remote-settings-template-preview
|
|
129
|
+
{
|
|
130
|
+
font-size: 0.68rem;
|
|
131
|
+
color: var(--retold-text-dim);
|
|
132
|
+
margin-top: 3px;
|
|
133
|
+
font-style: italic;
|
|
134
|
+
word-break: break-all;
|
|
135
|
+
}
|
|
100
136
|
`
|
|
101
137
|
};
|
|
102
138
|
|
|
@@ -306,6 +342,60 @@ class RetoldRemoteSettingsPanelView extends libPictView
|
|
|
306
342
|
tmpHTML += '</div>';
|
|
307
343
|
tmpHTML += '</div>'; // end capabilities section
|
|
308
344
|
|
|
345
|
+
// AI File Sort
|
|
346
|
+
tmpHTML += '<div class="retold-remote-settings-section">';
|
|
347
|
+
tmpHTML += '<div class="retold-remote-settings-section-title">AI File Sort</div>';
|
|
348
|
+
|
|
349
|
+
let tmpAISortManager = this.pict.providers['RetoldRemote-AISortManager'];
|
|
350
|
+
let tmpAISettings = tmpRemote.AISortSettings ||
|
|
351
|
+
{
|
|
352
|
+
AIEndpoint: 'http://localhost:11434',
|
|
353
|
+
AIModel: 'llama3.1',
|
|
354
|
+
AIProvider: 'ollama',
|
|
355
|
+
NamingTemplate: '{artist}/{album}/{track} - {title}'
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// AI Endpoint
|
|
359
|
+
tmpHTML += '<div class="retold-remote-settings-input-row">';
|
|
360
|
+
tmpHTML += '<label class="retold-remote-settings-input-label">AI Endpoint URL</label>';
|
|
361
|
+
tmpHTML += '<input class="retold-remote-settings-input" type="text" id="RetoldRemote-AISortEndpoint" value="' + this._escapeAttr(tmpAISettings.AIEndpoint) + '" onchange="pict.views[\'RetoldRemote-SettingsPanel\'].changeAISetting(\'AIEndpoint\', this.value)" placeholder="http://localhost:11434">';
|
|
362
|
+
tmpHTML += '</div>';
|
|
363
|
+
|
|
364
|
+
// AI Model
|
|
365
|
+
tmpHTML += '<div class="retold-remote-settings-input-row">';
|
|
366
|
+
tmpHTML += '<label class="retold-remote-settings-input-label">Model</label>';
|
|
367
|
+
tmpHTML += '<input class="retold-remote-settings-input" type="text" id="RetoldRemote-AISortModel" value="' + this._escapeAttr(tmpAISettings.AIModel) + '" onchange="pict.views[\'RetoldRemote-SettingsPanel\'].changeAISetting(\'AIModel\', this.value)" placeholder="llama3.1">';
|
|
368
|
+
tmpHTML += '</div>';
|
|
369
|
+
|
|
370
|
+
// AI Provider
|
|
371
|
+
tmpHTML += '<div class="retold-remote-settings-row">';
|
|
372
|
+
tmpHTML += '<span class="retold-remote-settings-label">Provider</span>';
|
|
373
|
+
tmpHTML += '<select class="retold-remote-settings-select" onchange="pict.views[\'RetoldRemote-SettingsPanel\'].changeAISetting(\'AIProvider\', this.value)">';
|
|
374
|
+
tmpHTML += '<option value="ollama"' + (tmpAISettings.AIProvider === 'ollama' ? ' selected' : '') + '>Ollama</option>';
|
|
375
|
+
tmpHTML += '<option value="openai"' + (tmpAISettings.AIProvider === 'openai' ? ' selected' : '') + '>OpenAI-compatible</option>';
|
|
376
|
+
tmpHTML += '</select>';
|
|
377
|
+
tmpHTML += '</div>';
|
|
378
|
+
|
|
379
|
+
// Naming Template
|
|
380
|
+
tmpHTML += '<div class="retold-remote-settings-input-row" style="margin-top: 8px;">';
|
|
381
|
+
tmpHTML += '<label class="retold-remote-settings-input-label">Naming Template</label>';
|
|
382
|
+
tmpHTML += '<input class="retold-remote-settings-input" type="text" id="RetoldRemote-AISortTemplate" value="' + this._escapeAttr(tmpAISettings.NamingTemplate) + '" onchange="pict.views[\'RetoldRemote-SettingsPanel\'].changeAISetting(\'NamingTemplate\', this.value)" placeholder="{artist}/{album}/{track} - {title}">';
|
|
383
|
+
|
|
384
|
+
// Template preview
|
|
385
|
+
let tmpTemplatePreview = tmpAISortManager ? tmpAISortManager.getTemplatePreview(tmpAISettings.NamingTemplate) : '';
|
|
386
|
+
if (tmpTemplatePreview)
|
|
387
|
+
{
|
|
388
|
+
tmpHTML += '<div class="retold-remote-settings-template-preview">Preview: ' + this._escapeHTML(tmpTemplatePreview) + '</div>';
|
|
389
|
+
}
|
|
390
|
+
tmpHTML += '</div>';
|
|
391
|
+
|
|
392
|
+
// Test Connection button
|
|
393
|
+
tmpHTML += '<button class="retold-remote-settings-vlc-btn" id="RetoldRemote-AISortTestBtn" onclick="pict.views[\'RetoldRemote-SettingsPanel\'].testAIConnection()" style="margin-top: 8px;">';
|
|
394
|
+
tmpHTML += 'Test Connection';
|
|
395
|
+
tmpHTML += '</button>';
|
|
396
|
+
|
|
397
|
+
tmpHTML += '</div>'; // end AI sort section
|
|
398
|
+
|
|
309
399
|
// VLC Setup
|
|
310
400
|
tmpHTML += '<div class="retold-remote-settings-section">';
|
|
311
401
|
tmpHTML += '<div class="retold-remote-settings-section-title">VLC Streaming</div>';
|
|
@@ -435,6 +525,72 @@ class RetoldRemoteSettingsPanelView extends libPictView
|
|
|
435
525
|
this._refilterGallery();
|
|
436
526
|
}
|
|
437
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Change an AI sort setting.
|
|
530
|
+
*
|
|
531
|
+
* @param {string} pKey - Setting key (AIEndpoint, AIModel, AIProvider, NamingTemplate)
|
|
532
|
+
* @param {string} pValue - New value
|
|
533
|
+
*/
|
|
534
|
+
changeAISetting(pKey, pValue)
|
|
535
|
+
{
|
|
536
|
+
let tmpAISortManager = this.pict.providers['RetoldRemote-AISortManager'];
|
|
537
|
+
if (tmpAISortManager)
|
|
538
|
+
{
|
|
539
|
+
let tmpUpdate = {};
|
|
540
|
+
tmpUpdate[pKey] = pValue;
|
|
541
|
+
tmpAISortManager.updateSettings(tmpUpdate);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Update the template preview if the template changed
|
|
545
|
+
if (pKey === 'NamingTemplate')
|
|
546
|
+
{
|
|
547
|
+
this._renderSettingsContent();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Test the AI endpoint connection.
|
|
553
|
+
*/
|
|
554
|
+
testAIConnection()
|
|
555
|
+
{
|
|
556
|
+
let tmpBtn = document.getElementById('RetoldRemote-AISortTestBtn');
|
|
557
|
+
if (tmpBtn)
|
|
558
|
+
{
|
|
559
|
+
tmpBtn.disabled = true;
|
|
560
|
+
tmpBtn.textContent = 'Testing...';
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
let tmpAISortManager = this.pict.providers['RetoldRemote-AISortManager'];
|
|
564
|
+
if (tmpAISortManager)
|
|
565
|
+
{
|
|
566
|
+
tmpAISortManager.testConnection();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Escape HTML attribute values.
|
|
572
|
+
*
|
|
573
|
+
* @param {string} pStr
|
|
574
|
+
* @returns {string}
|
|
575
|
+
*/
|
|
576
|
+
_escapeAttr(pStr)
|
|
577
|
+
{
|
|
578
|
+
if (!pStr) return '';
|
|
579
|
+
return String(pStr).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Escape HTML text content.
|
|
584
|
+
*
|
|
585
|
+
* @param {string} pStr
|
|
586
|
+
* @returns {string}
|
|
587
|
+
*/
|
|
588
|
+
_escapeHTML(pStr)
|
|
589
|
+
{
|
|
590
|
+
if (!pStr) return '';
|
|
591
|
+
return String(pStr).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
592
|
+
}
|
|
593
|
+
|
|
438
594
|
/**
|
|
439
595
|
* Re-run the filter/sort pipeline and refresh the gallery.
|
|
440
596
|
*/
|