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.
@@ -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
- // Sort controls
648
- let tmpControls = document.createElement('div');
649
- tmpControls.className = 'retold-remote-collections-detail-controls';
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
- let tmpSortSelect = document.createElement('select');
652
- tmpSortSelect.className = 'retold-remote-collections-sort-select';
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
- let tmpSortOptions = [
655
- { value: 'manual', label: 'Manual' },
656
- { value: 'name', label: 'Name' },
657
- { value: 'modified', label: 'Date Added' },
658
- { value: 'type', label: 'Type' }
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
- 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)
830
+ let tmpBtnRow = document.createElement('div');
831
+ tmpBtnRow.className = 'retold-remote-collections-op-buttons';
832
+
833
+ if (tmpPending > 0)
667
834
  {
668
- tmpOpt.selected = true;
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
- tmpSortSelect.onchange = (pEvent) =>
674
- {
675
- tmpManager.sortActiveCollection(pEvent.target.value, null);
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
- 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 = () =>
861
+ tmpOpControls.appendChild(tmpBtnRow);
862
+ pRoot.appendChild(tmpOpControls);
863
+ }
864
+ else
683
865
  {
684
- let tmpNewDir = (tmpRemote.ActiveCollection.SortDirection === 'desc') ? 'asc' : 'desc';
685
- tmpManager.sortActiveCollection(null, tmpNewDir);
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
- tmpControls.appendChild(tmpSortSelect);
689
- tmpControls.appendChild(tmpDirBtn);
690
- pRoot.appendChild(tmpControls);
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
- this._renderItemList(tmpBody, tmpCollection);
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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
592
+ }
593
+
438
594
  /**
439
595
  * Re-run the filter/sort pipeline and refresh the gallery.
440
596
  */