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.
@@ -12,6 +12,7 @@ html, body
12
12
  margin: 0;
13
13
  padding: 0;
14
14
  height: 100%;
15
+ height: 100dvh;
15
16
  overflow: hidden;
16
17
  background: var(--retold-bg-primary);
17
18
  color: var(--retold-text-primary);
@@ -20,6 +21,8 @@ html, body
20
21
  line-height: 1.5;
21
22
  -webkit-font-smoothing: antialiased;
22
23
  -moz-osx-font-smoothing: grayscale;
24
+ /* Safe area insets for notched devices */
25
+ padding-bottom: env(safe-area-inset-bottom);
23
26
  }
24
27
 
25
28
  /* Scrollbar styling */
package/html/index.html CHANGED
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
6
  <title>Retold Remote</title>
7
7
  <link rel="stylesheet" href="css/retold-remote.css">
8
8
  <style id="PICT-CSS"></style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retold-remote",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Retold Remote - NAS media browser with gallery views and keyboard navigation",
5
5
  "main": "source/Pict-RetoldRemote-Bundle.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@ const libProviderRetoldRemoteTheme = require('./providers/Pict-Provider-RetoldRe
11
11
  const libProviderFormattingUtilities = require('./providers/Pict-Provider-FormattingUtilities.js');
12
12
  const libProviderToastNotification = require('./providers/Pict-Provider-ToastNotification.js');
13
13
  const libProviderCollectionManager = require('./providers/Pict-Provider-CollectionManager.js');
14
+ const libProviderAISortManager = require('./providers/Pict-Provider-AISortManager.js');
14
15
 
15
16
  // Views (replace parent views)
16
17
  const libViewLayout = require('./views/PictView-Remote-Layout.js');
@@ -68,6 +69,7 @@ class RetoldRemoteApplication extends libContentEditorApplication
68
69
  this.pict.addProvider('RetoldRemote-FormattingUtilities', libProviderFormattingUtilities.default_configuration, libProviderFormattingUtilities);
69
70
  this.pict.addProvider('RetoldRemote-ToastNotification', libProviderToastNotification.default_configuration, libProviderToastNotification);
70
71
  this.pict.addProvider('RetoldRemote-CollectionManager', libProviderCollectionManager.default_configuration, libProviderCollectionManager);
72
+ this.pict.addProvider('RetoldRemote-AISortManager', libProviderAISortManager.default_configuration, libProviderAISortManager);
71
73
  }
72
74
 
73
75
  onAfterInitializeAsync(fCallback)
@@ -145,7 +147,16 @@ class RetoldRemoteApplication extends libContentEditorApplication
145
147
  CollectionSearchQuery: '',
146
148
  LastUsedCollectionGUID: null,
147
149
  BrowsingCollection: false, // true when viewer is navigating collection items
148
- BrowsingCollectionIndex: -1 // index into ActiveCollection.Items
150
+ BrowsingCollectionIndex: -1, // index into ActiveCollection.Items
151
+
152
+ // AI Sort settings
153
+ AISortSettings:
154
+ {
155
+ AIEndpoint: 'http://localhost:11434',
156
+ AIModel: 'llama3.1',
157
+ AIProvider: 'ollama',
158
+ NamingTemplate: '{artist}/{album}/{track} - {title}'
159
+ }
149
160
  };
150
161
 
151
162
  // Load persisted settings
@@ -746,7 +757,8 @@ class RetoldRemoteApplication extends libContentEditorApplication
746
757
  ListShowDate: tmpRemote.ListShowDate,
747
758
  CollectionsPanelOpen: tmpRemote.CollectionsPanelOpen,
748
759
  CollectionsPanelWidth: tmpRemote.CollectionsPanelWidth,
749
- LastUsedCollectionGUID: tmpRemote.LastUsedCollectionGUID
760
+ LastUsedCollectionGUID: tmpRemote.LastUsedCollectionGUID,
761
+ AISortSettings: tmpRemote.AISortSettings
750
762
  };
751
763
  localStorage.setItem('retold-remote-settings', JSON.stringify(tmpSettings));
752
764
  }
@@ -794,6 +806,13 @@ class RetoldRemoteApplication extends libContentEditorApplication
794
806
  if (typeof (tmpSettings.CollectionsPanelOpen) === 'boolean') tmpRemote.CollectionsPanelOpen = tmpSettings.CollectionsPanelOpen;
795
807
  if (tmpSettings.CollectionsPanelWidth) tmpRemote.CollectionsPanelWidth = tmpSettings.CollectionsPanelWidth;
796
808
  if (tmpSettings.LastUsedCollectionGUID) tmpRemote.LastUsedCollectionGUID = tmpSettings.LastUsedCollectionGUID;
809
+ if (tmpSettings.AISortSettings && typeof tmpSettings.AISortSettings === 'object')
810
+ {
811
+ if (tmpSettings.AISortSettings.AIEndpoint) tmpRemote.AISortSettings.AIEndpoint = tmpSettings.AISortSettings.AIEndpoint;
812
+ if (tmpSettings.AISortSettings.AIModel) tmpRemote.AISortSettings.AIModel = tmpSettings.AISortSettings.AIModel;
813
+ if (tmpSettings.AISortSettings.AIProvider) tmpRemote.AISortSettings.AIProvider = tmpSettings.AISortSettings.AIProvider;
814
+ if (tmpSettings.AISortSettings.NamingTemplate) tmpRemote.AISortSettings.NamingTemplate = tmpSettings.AISortSettings.NamingTemplate;
815
+ }
797
816
  }
798
817
  }
799
818
  catch (pError)
@@ -43,6 +43,9 @@ const libRetoldRemoteVideoFrameService = require('../server/RetoldRemote-VideoFr
43
43
  const libRetoldRemoteAudioWaveformService = require('../server/RetoldRemote-AudioWaveformService.js');
44
44
  const libRetoldRemoteEbookService = require('../server/RetoldRemote-EbookService.js');
45
45
  const libRetoldRemoteCollectionService = require('../server/RetoldRemote-CollectionService.js');
46
+ const libRetoldRemoteMetadataCache = require('../server/RetoldRemote-MetadataCache.js');
47
+ const libRetoldRemoteFileOperationService = require('../server/RetoldRemote-FileOperationService.js');
48
+ const libRetoldRemoteAISortService = require('../server/RetoldRemote-AISortService.js');
46
49
  const libUrl = require('url');
47
50
 
48
51
  function setupRetoldRemoteServer(pOptions, fCallback)
@@ -151,8 +154,28 @@ function setupRetoldRemoteServer(pOptions, fCallback)
151
154
  ContentPath: tmpContentPath
152
155
  });
153
156
 
157
+ // Set up the metadata cache service
158
+ let tmpMetadataCache = new libRetoldRemoteMetadataCache(tmpFable,
159
+ {
160
+ ContentPath: tmpContentPath
161
+ });
162
+
163
+ // Set up the file operation service
164
+ let tmpFileOperationService = new libRetoldRemoteFileOperationService(tmpFable,
165
+ {
166
+ ContentPath: tmpContentPath
167
+ });
168
+
169
+ // Set up the AI sort service
170
+ let tmpAISortService = new libRetoldRemoteAISortService(tmpFable,
171
+ {
172
+ ContentPath: tmpContentPath
173
+ });
174
+ tmpAISortService.setMetadataCache(tmpMetadataCache);
175
+
154
176
  // Set up the collection service
155
177
  let tmpCollectionService = new libRetoldRemoteCollectionService(tmpFable, {});
178
+ tmpCollectionService.setFileOperationService(tmpFileOperationService);
156
179
 
157
180
  // Set up the media service
158
181
  let tmpMediaService = new libRetoldRemoteMediaService(tmpFable,
@@ -333,9 +356,113 @@ function setupRetoldRemoteServer(pOptions, fCallback)
333
356
  // Connect media service API routes
334
357
  tmpMediaService.connectRoutes(tmpServiceServer);
335
358
 
359
+ // Connect file operation service API routes
360
+ tmpFileOperationService.connectRoutes(tmpServiceServer);
361
+
336
362
  // Connect collection service API routes
337
363
  tmpCollectionService.connectRoutes(tmpServiceServer);
338
364
 
365
+ // Connect AI sort service API routes
366
+ tmpAISortService.connectRoutes(tmpServiceServer);
367
+
368
+ // --- GET /api/media/metadata ---
369
+ // Get cached metadata (with ID3/format tags) for a single file.
370
+ tmpServiceServer.get('/api/media/metadata',
371
+ (pRequest, pResponse, fNext) =>
372
+ {
373
+ try
374
+ {
375
+ let tmpParsedUrl = libUrl.parse(pRequest.url, true);
376
+ let tmpQuery = tmpParsedUrl.query;
377
+ let tmpRelPath = tmpQuery.path;
378
+
379
+ if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
380
+ {
381
+ pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
382
+ return fNext();
383
+ }
384
+
385
+ tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
386
+ if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
387
+ {
388
+ pResponse.send(400, { Success: false, Error: 'Invalid path.' });
389
+ return fNext();
390
+ }
391
+
392
+ tmpMetadataCache.getMetadata(tmpRelPath,
393
+ (pError, pMetadata) =>
394
+ {
395
+ if (pError)
396
+ {
397
+ pResponse.send(404, { Success: false, Error: pError.message });
398
+ return fNext();
399
+ }
400
+ pResponse.send(pMetadata);
401
+ return fNext();
402
+ });
403
+ }
404
+ catch (pError)
405
+ {
406
+ pResponse.send(500, { Success: false, Error: pError.message });
407
+ return fNext();
408
+ }
409
+ });
410
+
411
+ // --- POST /api/media/metadata-batch ---
412
+ // Get cached metadata for multiple files at once.
413
+ tmpServiceServer.post('/api/media/metadata-batch',
414
+ (pRequest, pResponse, fNext) =>
415
+ {
416
+ try
417
+ {
418
+ let tmpPaths = pRequest.body && pRequest.body.Paths;
419
+
420
+ if (!Array.isArray(tmpPaths) || tmpPaths.length === 0)
421
+ {
422
+ pResponse.send(400, { Success: false, Error: 'Missing Paths array.' });
423
+ return fNext();
424
+ }
425
+
426
+ // Sanitize all paths
427
+ let tmpSanitized = [];
428
+ for (let i = 0; i < tmpPaths.length; i++)
429
+ {
430
+ let tmpRelPath = (typeof (tmpPaths[i]) === 'string')
431
+ ? decodeURIComponent(tmpPaths[i]).replace(/^\/+/, '')
432
+ : null;
433
+
434
+ if (!tmpRelPath || tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
435
+ {
436
+ continue;
437
+ }
438
+ tmpSanitized.push(tmpRelPath);
439
+ }
440
+
441
+ if (tmpSanitized.length === 0)
442
+ {
443
+ pResponse.send(400, { Success: false, Error: 'No valid paths.' });
444
+ return fNext();
445
+ }
446
+
447
+ tmpMetadataCache.getMetadataBatch(tmpSanitized,
448
+ (pError, pResults) =>
449
+ {
450
+ if (pError)
451
+ {
452
+ pResponse.send(500, { Success: false, Error: pError.message });
453
+ return fNext();
454
+ }
455
+ pResponse.send({ Success: true, Files: pResults });
456
+ return fNext();
457
+ });
458
+ }
459
+ catch (pError)
460
+ {
461
+ pResponse.send(500, { Success: false, Error: pError.message });
462
+ return fNext();
463
+ }
464
+ });
465
+
339
466
  // --- GET /api/media/video-frames ---
340
467
  // Extract evenly-spaced frames from a video for the Video Explorer.
341
468
  tmpServiceServer.get('/api/media/video-frames',
@@ -1044,6 +1171,8 @@ function setupRetoldRemoteServer(pOptions, fCallback)
1044
1171
  AudioWaveformService: tmpAudioWaveformService,
1045
1172
  PathRegistry: tmpPathRegistry,
1046
1173
  ParimeCache: tmpParimeCache,
1174
+ MetadataCache: tmpMetadataCache,
1175
+ FileOperationService: tmpFileOperationService,
1047
1176
  Port: tmpPort
1048
1177
  });
1049
1178
  });
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Retold Remote -- AI Sort Manager Provider
3
+ *
4
+ * Client-side state management and API communication for the
5
+ * AI-powered file sorting feature. Manages AI endpoint settings,
6
+ * connection testing, folder scanning, and sort plan generation.
7
+ *
8
+ * Sort plans are created as operation-plan collections, so all
9
+ * preview, editing, and execution happens through the existing
10
+ * collections infrastructure.
11
+ *
12
+ * Settings are persisted via localStorage alongside other RetoldRemote
13
+ * settings.
14
+ *
15
+ * @license MIT
16
+ */
17
+ const libPictProvider = require('pict-provider');
18
+
19
+ const _DefaultProviderConfiguration =
20
+ {
21
+ ProviderIdentifier: 'RetoldRemote-AISortManager',
22
+ AutoInitialize: true,
23
+ AutoSolveWithApp: false
24
+ };
25
+
26
+ class AISortManagerProvider extends libPictProvider
27
+ {
28
+ constructor(pFable, pOptions, pServiceHash)
29
+ {
30
+ super(pFable, pOptions, pServiceHash);
31
+
32
+ // Track in-progress operations
33
+ this._generating = false;
34
+ this._scanning = false;
35
+ this._testingConnection = false;
36
+ }
37
+
38
+ // -- State Accessors --------------------------------------------------
39
+
40
+ /**
41
+ * Shortcut to the RetoldRemote AppData namespace.
42
+ */
43
+ _getRemote()
44
+ {
45
+ return this.pict.AppData.RetoldRemote;
46
+ }
47
+
48
+ /**
49
+ * Get the AI sort settings from AppData.
50
+ */
51
+ _getSettings()
52
+ {
53
+ let tmpRemote = this._getRemote();
54
+ if (!tmpRemote.AISortSettings)
55
+ {
56
+ tmpRemote.AISortSettings =
57
+ {
58
+ AIEndpoint: 'http://localhost:11434',
59
+ AIModel: 'llama3.1',
60
+ AIProvider: 'ollama',
61
+ NamingTemplate: '{artist}/{album}/{track} - {title}'
62
+ };
63
+ }
64
+ return tmpRemote.AISortSettings;
65
+ }
66
+
67
+ /**
68
+ * Get the toast notification provider.
69
+ */
70
+ _getToast()
71
+ {
72
+ return this.pict.providers['RetoldRemote-ToastNotification'];
73
+ }
74
+
75
+ /**
76
+ * Get the collection manager provider.
77
+ */
78
+ _getCollectionManager()
79
+ {
80
+ return this.pict.providers['RetoldRemote-CollectionManager'];
81
+ }
82
+
83
+ // -- Settings Management ----------------------------------------------
84
+
85
+ /**
86
+ * Update AI sort settings.
87
+ *
88
+ * @param {object} pSettings - Settings object with any of: AIEndpoint, AIModel, AIProvider, NamingTemplate
89
+ */
90
+ updateSettings(pSettings)
91
+ {
92
+ let tmpSettings = this._getSettings();
93
+
94
+ if (pSettings.AIEndpoint !== undefined)
95
+ {
96
+ tmpSettings.AIEndpoint = pSettings.AIEndpoint;
97
+ }
98
+ if (pSettings.AIModel !== undefined)
99
+ {
100
+ tmpSettings.AIModel = pSettings.AIModel;
101
+ }
102
+ if (pSettings.AIProvider !== undefined)
103
+ {
104
+ tmpSettings.AIProvider = pSettings.AIProvider;
105
+ }
106
+ if (pSettings.NamingTemplate !== undefined)
107
+ {
108
+ tmpSettings.NamingTemplate = pSettings.NamingTemplate;
109
+ }
110
+
111
+ // Persist settings
112
+ this.pict.PictApplication.saveSettings();
113
+ }
114
+
115
+ // -- API Methods ------------------------------------------------------
116
+
117
+ /**
118
+ * Test the AI endpoint connection.
119
+ *
120
+ * @param {Function} [fCallback] - Optional callback(pError, pResult)
121
+ */
122
+ testConnection(fCallback)
123
+ {
124
+ let tmpSelf = this;
125
+ let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
126
+
127
+ if (this._testingConnection)
128
+ {
129
+ return tmpCallback(new Error('Connection test already in progress'));
130
+ }
131
+
132
+ this._testingConnection = true;
133
+ let tmpSettings = this._getSettings();
134
+
135
+ let tmpToast = this._getToast();
136
+
137
+ fetch('/api/ai-sort/test-connection',
138
+ {
139
+ method: 'POST',
140
+ headers: { 'Content-Type': 'application/json' },
141
+ body: JSON.stringify(
142
+ {
143
+ AIEndpoint: tmpSettings.AIEndpoint,
144
+ AIModel: tmpSettings.AIModel,
145
+ AIProvider: tmpSettings.AIProvider
146
+ })
147
+ })
148
+ .then((pResponse) => pResponse.json())
149
+ .then((pData) =>
150
+ {
151
+ tmpSelf._testingConnection = false;
152
+
153
+ if (pData.Success)
154
+ {
155
+ if (tmpToast)
156
+ {
157
+ tmpToast.show('AI connected (' + pData.ResponseTime + 'ms)');
158
+ }
159
+ }
160
+ else
161
+ {
162
+ if (tmpToast)
163
+ {
164
+ tmpToast.show('AI connection failed: ' + (pData.Error || 'Unknown error'));
165
+ }
166
+ }
167
+
168
+ // Update the test button state if settings panel is open
169
+ tmpSelf._updateTestButtonState(pData.Success);
170
+
171
+ return tmpCallback(null, pData);
172
+ })
173
+ .catch((pError) =>
174
+ {
175
+ tmpSelf._testingConnection = false;
176
+
177
+ if (tmpToast)
178
+ {
179
+ tmpToast.show('AI connection error: ' + pError.message);
180
+ }
181
+
182
+ tmpSelf._updateTestButtonState(false);
183
+
184
+ return tmpCallback(pError);
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Scan a folder for audio files and return metadata.
190
+ *
191
+ * @param {string} pPath - Folder path relative to content root
192
+ * @param {boolean} [pRecursive] - Scan subdirectories
193
+ * @param {Function} [fCallback] - Optional callback(pError, pResult)
194
+ */
195
+ scanFolder(pPath, pRecursive, fCallback)
196
+ {
197
+ let tmpSelf = this;
198
+ let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
199
+
200
+ if (this._scanning)
201
+ {
202
+ return tmpCallback(new Error('Scan already in progress'));
203
+ }
204
+
205
+ this._scanning = true;
206
+
207
+ fetch('/api/ai-sort/scan',
208
+ {
209
+ method: 'POST',
210
+ headers: { 'Content-Type': 'application/json' },
211
+ body: JSON.stringify(
212
+ {
213
+ Path: pPath || '',
214
+ Recursive: pRecursive || false
215
+ })
216
+ })
217
+ .then((pResponse) => pResponse.json())
218
+ .then((pData) =>
219
+ {
220
+ tmpSelf._scanning = false;
221
+
222
+ let tmpToast = tmpSelf._getToast();
223
+ if (tmpToast)
224
+ {
225
+ if (pData.Success)
226
+ {
227
+ tmpToast.show('Scanned ' + pData.FileCount + ' audio files');
228
+ }
229
+ else
230
+ {
231
+ tmpToast.show('Scan failed: ' + (pData.Error || 'Unknown error'));
232
+ }
233
+ }
234
+
235
+ return tmpCallback(null, pData);
236
+ })
237
+ .catch((pError) =>
238
+ {
239
+ tmpSelf._scanning = false;
240
+
241
+ let tmpToast = tmpSelf._getToast();
242
+ if (tmpToast)
243
+ {
244
+ tmpToast.show('Scan error: ' + pError.message);
245
+ }
246
+
247
+ return tmpCallback(pError);
248
+ });
249
+ }
250
+
251
+ /**
252
+ * Generate a sort plan for a folder.
253
+ *
254
+ * Creates an operation-plan collection and opens it in the
255
+ * collections panel for preview and editing.
256
+ *
257
+ * @param {string} pPath - Folder path relative to content root
258
+ * @param {Function} [fCallback] - Optional callback(pError, pResult)
259
+ */
260
+ generateSortPlan(pPath, fCallback)
261
+ {
262
+ let tmpSelf = this;
263
+ let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
264
+
265
+ if (this._generating)
266
+ {
267
+ let tmpToast = this._getToast();
268
+ if (tmpToast)
269
+ {
270
+ tmpToast.show('Sort plan generation already in progress...');
271
+ }
272
+ return tmpCallback(new Error('Generation already in progress'));
273
+ }
274
+
275
+ this._generating = true;
276
+ let tmpSettings = this._getSettings();
277
+
278
+ let tmpToast = this._getToast();
279
+ if (tmpToast)
280
+ {
281
+ tmpToast.show('Generating sort plan...');
282
+ }
283
+
284
+ fetch('/api/ai-sort/generate-plan',
285
+ {
286
+ method: 'POST',
287
+ headers: { 'Content-Type': 'application/json' },
288
+ body: JSON.stringify(
289
+ {
290
+ Path: pPath || '',
291
+ NamingTemplate: tmpSettings.NamingTemplate,
292
+ Recursive: true,
293
+ AIEndpoint: tmpSettings.AIEndpoint,
294
+ AIModel: tmpSettings.AIModel,
295
+ AIProvider: tmpSettings.AIProvider
296
+ })
297
+ })
298
+ .then((pResponse) => pResponse.json())
299
+ .then((pData) =>
300
+ {
301
+ tmpSelf._generating = false;
302
+
303
+ if (!pData.Success)
304
+ {
305
+ if (tmpToast)
306
+ {
307
+ tmpToast.show('Sort plan failed: ' + (pData.Error || pData.Message || 'Unknown error'));
308
+ }
309
+ return tmpCallback(new Error(pData.Error || pData.Message));
310
+ }
311
+
312
+ if (!pData.CollectionGUID)
313
+ {
314
+ if (tmpToast)
315
+ {
316
+ tmpToast.show(pData.Message || 'No audio files found');
317
+ }
318
+ return tmpCallback(null, pData);
319
+ }
320
+
321
+ // Open the generated collection in the panel
322
+ let tmpCollManager = tmpSelf._getCollectionManager();
323
+ if (tmpCollManager)
324
+ {
325
+ let tmpRemote = tmpSelf._getRemote();
326
+ tmpRemote.CollectionsPanelMode = 'detail';
327
+
328
+ if (!tmpRemote.CollectionsPanelOpen)
329
+ {
330
+ tmpCollManager.togglePanel();
331
+ }
332
+
333
+ tmpCollManager.fetchCollection(pData.CollectionGUID);
334
+ tmpCollManager.fetchCollections();
335
+ }
336
+
337
+ if (tmpToast)
338
+ {
339
+ let tmpMsg = 'Sort plan created: ' + pData.TotalFiles + ' files';
340
+ if (pData.TaggedFiles > 0)
341
+ {
342
+ tmpMsg += ' (' + pData.TaggedFiles + ' by tags';
343
+ if (pData.AIFiles > 0)
344
+ {
345
+ tmpMsg += ', ' + pData.AIFiles + ' by AI';
346
+ }
347
+ tmpMsg += ')';
348
+ }
349
+ tmpToast.show(tmpMsg);
350
+ }
351
+
352
+ return tmpCallback(null, pData);
353
+ })
354
+ .catch((pError) =>
355
+ {
356
+ tmpSelf._generating = false;
357
+
358
+ if (tmpToast)
359
+ {
360
+ tmpToast.show('Sort plan error: ' + pError.message);
361
+ }
362
+
363
+ return tmpCallback(pError);
364
+ });
365
+ }
366
+
367
+ // -- UI Helpers --------------------------------------------------------
368
+
369
+ /**
370
+ * Update the test connection button state in the settings panel.
371
+ *
372
+ * @param {boolean} pSuccess - Whether the test was successful
373
+ */
374
+ _updateTestButtonState(pSuccess)
375
+ {
376
+ let tmpBtn = document.getElementById('RetoldRemote-AISortTestBtn');
377
+ if (!tmpBtn)
378
+ {
379
+ return;
380
+ }
381
+
382
+ tmpBtn.disabled = false;
383
+
384
+ if (pSuccess === true)
385
+ {
386
+ tmpBtn.style.borderColor = 'var(--retold-accent)';
387
+ tmpBtn.style.color = 'var(--retold-accent)';
388
+ tmpBtn.textContent = 'Connected';
389
+ setTimeout(() =>
390
+ {
391
+ tmpBtn.textContent = 'Test Connection';
392
+ tmpBtn.style.borderColor = '';
393
+ tmpBtn.style.color = '';
394
+ }, 3000);
395
+ }
396
+ else if (pSuccess === false)
397
+ {
398
+ tmpBtn.style.borderColor = 'var(--retold-danger-muted)';
399
+ tmpBtn.style.color = 'var(--retold-danger-muted)';
400
+ tmpBtn.textContent = 'Failed';
401
+ setTimeout(() =>
402
+ {
403
+ tmpBtn.textContent = 'Test Connection';
404
+ tmpBtn.style.borderColor = '';
405
+ tmpBtn.style.color = '';
406
+ }, 3000);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Get a preview of the naming template with sample data.
412
+ *
413
+ * @param {string} pTemplate - Naming template
414
+ * @returns {string} Preview string
415
+ */
416
+ getTemplatePreview(pTemplate)
417
+ {
418
+ let tmpTemplate = pTemplate || '{artist}/{album}/{track} - {title}';
419
+ let tmpPreview = tmpTemplate
420
+ .replace(/\{artist\}/gi, 'Pink Floyd')
421
+ .replace(/\{album\}/gi, 'Dark Side of the Moon')
422
+ .replace(/\{title\}/gi, 'Time')
423
+ .replace(/\{track\}/gi, '04')
424
+ .replace(/\{year\}/gi, '1973')
425
+ .replace(/\{genre\}/gi, 'Progressive Rock');
426
+
427
+ return tmpPreview + '.mp3';
428
+ }
429
+
430
+ /**
431
+ * Whether the AI sort button should be visible for the current folder.
432
+ *
433
+ * Returns true if we're in gallery mode (browsing a folder).
434
+ *
435
+ * @returns {boolean}
436
+ */
437
+ isAvailable()
438
+ {
439
+ let tmpRemote = this._getRemote();
440
+ return tmpRemote.ActiveMode === 'gallery';
441
+ }
442
+
443
+ /**
444
+ * Whether a generation is currently in progress.
445
+ *
446
+ * @returns {boolean}
447
+ */
448
+ isGenerating()
449
+ {
450
+ return this._generating;
451
+ }
452
+ }
453
+
454
+ AISortManagerProvider.default_configuration = _DefaultProviderConfiguration;
455
+
456
+ module.exports = AISortManagerProvider;