@duplicati/ngclient 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 (152) hide show
  1. package/.dockerignore +13 -0
  2. package/.github/workflows/npm-publish.yml +59 -0
  3. package/.prettierignore +13 -0
  4. package/.prettierrc +12 -0
  5. package/.vscode/extensions.json +4 -0
  6. package/.vscode/launch.json +13 -0
  7. package/.vscode/settings.json +8 -0
  8. package/.vscode/tasks.json +24 -0
  9. package/Dockerfile +38 -0
  10. package/LICENSE +21 -0
  11. package/README.md +27 -0
  12. package/angular.json +98 -0
  13. package/bun.lockb +0 -0
  14. package/openapi-ts.config.ts +13 -0
  15. package/package.json +46 -0
  16. package/scripts/generate-package-json.ts +15 -0
  17. package/src/app/about/about.component.html +15 -0
  18. package/src/app/about/about.component.scss +19 -0
  19. package/src/app/about/about.component.ts +13 -0
  20. package/src/app/about/changelog/changelog.component.html +3 -0
  21. package/src/app/about/changelog/changelog.component.scss +7 -0
  22. package/src/app/about/changelog/changelog.component.ts +18 -0
  23. package/src/app/about/general/general.component.html +9 -0
  24. package/src/app/about/general/general.component.scss +23 -0
  25. package/src/app/about/general/general.component.ts +30 -0
  26. package/src/app/about/libraries/libraries.component.html +3 -0
  27. package/src/app/about/libraries/libraries.component.scss +7 -0
  28. package/src/app/about/libraries/libraries.component.ts +18 -0
  29. package/src/app/about/logs/logs.component.html +5 -0
  30. package/src/app/about/logs/logs.component.scss +7 -0
  31. package/src/app/about/logs/logs.component.ts +18 -0
  32. package/src/app/about/system-info/system-info.component.html +3 -0
  33. package/src/app/about/system-info/system-info.component.scss +7 -0
  34. package/src/app/about/system-info/system-info.component.ts +17 -0
  35. package/src/app/add-backup/add-backup.component.html +29 -0
  36. package/src/app/add-backup/add-backup.component.scss +38 -0
  37. package/src/app/add-backup/add-backup.component.ts +14 -0
  38. package/src/app/app.component.ts +35 -0
  39. package/src/app/app.config.ts +23 -0
  40. package/src/app/app.routes.ts +121 -0
  41. package/src/app/core/components/file-tree/file-tree.component.html +87 -0
  42. package/src/app/core/components/file-tree/file-tree.component.scss +80 -0
  43. package/src/app/core/components/file-tree/file-tree.component.ts +485 -0
  44. package/src/app/core/components/logo/logo.component.html +22 -0
  45. package/src/app/core/components/logo/logo.component.scss +12 -0
  46. package/src/app/core/components/logo/logo.component.ts +11 -0
  47. package/src/app/core/components/status-bar/status-bar.component.html +38 -0
  48. package/src/app/core/components/status-bar/status-bar.component.scss +27 -0
  49. package/src/app/core/components/status-bar/status-bar.component.ts +33 -0
  50. package/src/app/core/components/status-bar/status-bar.state.ts +182 -0
  51. package/src/app/core/components/toggle-card/toggle-card.component.html +11 -0
  52. package/src/app/core/components/toggle-card/toggle-card.component.scss +49 -0
  53. package/src/app/core/components/toggle-card/toggle-card.component.ts +28 -0
  54. package/src/app/core/interceptors/http.interceptor.ts +69 -0
  55. package/src/app/core/interceptors/websocket.interceptor.ts +66 -0
  56. package/src/app/core/openapi/core/ApiError.ts +21 -0
  57. package/src/app/core/openapi/core/ApiRequestOptions.ts +21 -0
  58. package/src/app/core/openapi/core/ApiResult.ts +7 -0
  59. package/src/app/core/openapi/core/OpenAPI.ts +55 -0
  60. package/src/app/core/openapi/core/request.ts +337 -0
  61. package/src/app/core/openapi/index.ts +6 -0
  62. package/src/app/core/openapi/schemas.gen.ts +3611 -0
  63. package/src/app/core/openapi/services.gen.ts +1460 -0
  64. package/src/app/core/openapi/types.gen.ts +1510 -0
  65. package/src/app/core/pipes/duration.pipe.ts +27 -0
  66. package/src/app/core/pipes/relative-time.pipe.ts +14 -0
  67. package/src/app/core/providers/dayjs.ts +16 -0
  68. package/src/app/core/services/dict.en.ts +0 -0
  69. package/src/app/core/services/dictionary.service.ts +12 -0
  70. package/src/app/core/services/localstorage.token.ts +82 -0
  71. package/src/app/core/services/password-generator.service.ts +39 -0
  72. package/src/app/core/services/relay-websocket.service.ts +268 -0
  73. package/src/app/core/services/timespan-literals.service.ts +25 -0
  74. package/src/app/core/states/app-auth.state.ts +87 -0
  75. package/src/app/core/states/backups.state.ts +71 -0
  76. package/src/app/core/states/relayconfig.state.ts +23 -0
  77. package/src/app/core/states/sysinfo.state.ts +31 -0
  78. package/src/app/core/states/tasks.state.ts +34 -0
  79. package/src/app/core/types/subscribed.ts +7 -0
  80. package/src/app/core/validators/custom.validators.ts +120 -0
  81. package/src/app/create-edit-backup/create-edit-backup.component.html +28 -0
  82. package/src/app/create-edit-backup/create-edit-backup.component.scss +15 -0
  83. package/src/app/create-edit-backup/create-edit-backup.component.ts +33 -0
  84. package/src/app/create-edit-backup/create-edit-backup.state.ts +582 -0
  85. package/src/app/create-edit-backup/destination/destination.component.html +367 -0
  86. package/src/app/create-edit-backup/destination/destination.component.scss +85 -0
  87. package/src/app/create-edit-backup/destination/destination.component.ts +249 -0
  88. package/src/app/create-edit-backup/destination/destination.config.ts +103 -0
  89. package/src/app/create-edit-backup/destination/destination.mapper.ts +177 -0
  90. package/src/app/create-edit-backup/general/general.component.html +108 -0
  91. package/src/app/create-edit-backup/general/general.component.scss +34 -0
  92. package/src/app/create-edit-backup/general/general.component.ts +136 -0
  93. package/src/app/create-edit-backup/options/options.component.html +95 -0
  94. package/src/app/create-edit-backup/options/options.component.scss +42 -0
  95. package/src/app/create-edit-backup/options/options.component.ts +113 -0
  96. package/src/app/create-edit-backup/schedule/schedule.component.html +102 -0
  97. package/src/app/create-edit-backup/schedule/schedule.component.scss +38 -0
  98. package/src/app/create-edit-backup/schedule/schedule.component.ts +137 -0
  99. package/src/app/create-edit-backup/source-data/source-data.component.html +93 -0
  100. package/src/app/create-edit-backup/source-data/source-data.component.scss +41 -0
  101. package/src/app/create-edit-backup/source-data/source-data.component.ts +114 -0
  102. package/src/app/home/backup.const.ts +167 -0
  103. package/src/app/home/home.component.html +98 -0
  104. package/src/app/home/home.component.scss +88 -0
  105. package/src/app/home/home.component.ts +54 -0
  106. package/src/app/layout/layout.component.html +64 -0
  107. package/src/app/layout/layout.component.scss +24 -0
  108. package/src/app/layout/layout.component.ts +51 -0
  109. package/src/app/layout/layout.state.ts +70 -0
  110. package/src/app/login/login.component.html +12 -0
  111. package/src/app/login/login.component.scss +25 -0
  112. package/src/app/login/login.component.ts +34 -0
  113. package/src/app/logout/logout.component.html +1 -0
  114. package/src/app/logout/logout.component.scss +3 -0
  115. package/src/app/logout/logout.component.ts +18 -0
  116. package/src/app/restore/restore.component.html +56 -0
  117. package/src/app/restore/restore.component.scss +37 -0
  118. package/src/app/restore/restore.component.ts +55 -0
  119. package/src/app/restore-flow/options/options.component.html +57 -0
  120. package/src/app/restore-flow/options/options.component.scss +16 -0
  121. package/src/app/restore-flow/options/options.component.ts +58 -0
  122. package/src/app/restore-flow/restore-flow.component.html +14 -0
  123. package/src/app/restore-flow/restore-flow.component.scss +15 -0
  124. package/src/app/restore-flow/restore-flow.component.ts +23 -0
  125. package/src/app/restore-flow/restore-flow.state.ts +120 -0
  126. package/src/app/restore-flow/select-files/select-files.component.html +42 -0
  127. package/src/app/restore-flow/select-files/select-files.component.scss +16 -0
  128. package/src/app/restore-flow/select-files/select-files.component.ts +129 -0
  129. package/src/app/settings/settings.component.html +256 -0
  130. package/src/app/settings/settings.component.scss +106 -0
  131. package/src/app/settings/settings.component.ts +475 -0
  132. package/src/assets/duplicati-logo.png +0 -0
  133. package/src/assets/duplicati-logo.svg +10 -0
  134. package/src/assets/favicon.ico +0 -0
  135. package/src/assets/images/backup.png +0 -0
  136. package/src/assets/images/dark-theme.png +0 -0
  137. package/src/assets/images/folder.png +0 -0
  138. package/src/assets/images/light-theme.png +0 -0
  139. package/src/assets/images/new-backup.png +0 -0
  140. package/src/assets/images/restore.png +0 -0
  141. package/src/assets/images/system-theme.png +0 -0
  142. package/src/assets/spk.css +33 -0
  143. package/src/assets/spk.woff2 +0 -0
  144. package/src/environments/environment-token.ts +4 -0
  145. package/src/environments/environment.prod.ts +6 -0
  146. package/src/environments/environment.ts +6 -0
  147. package/src/index.html +15 -0
  148. package/src/main.ts +6 -0
  149. package/src/proxy.conf.mjs +6 -0
  150. package/src/styles.scss +41 -0
  151. package/tsconfig.app.json +15 -0
  152. package/tsconfig.json +30 -0
@@ -0,0 +1,485 @@
1
+ import { JsonPipe, NgTemplateOutlet } from '@angular/common';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ computed,
6
+ effect,
7
+ ElementRef,
8
+ inject,
9
+ input,
10
+ output,
11
+ signal,
12
+ viewChild,
13
+ } from '@angular/core';
14
+ import { FormsModule } from '@angular/forms';
15
+ import {
16
+ SparkleButtonGroupComponent,
17
+ SparkleCheckboxComponent,
18
+ SparkleFormFieldComponent,
19
+ SparkleIconComponent,
20
+ SparkleListComponent,
21
+ SparkleToggleComponent,
22
+ } from '@sparkle-ui/core';
23
+ import { finalize, forkJoin, Observable } from 'rxjs';
24
+ import { DuplicatiServerService, GetApiV1BackupByIdFilesData, TreeNodeDto } from '../../openapi';
25
+
26
+ type TreeNode = TreeNodeDto & {
27
+ isSelected: boolean;
28
+ parentPath: string;
29
+ };
30
+
31
+ type FileTreeNode = TreeNode & {
32
+ children: FileTreeNode[];
33
+ };
34
+
35
+ type InputValueChangedEvent = CustomEvent<{ value: string }>;
36
+
37
+ interface CustomEventMap {
38
+ inputValueChanged: InputValueChangedEvent;
39
+ }
40
+
41
+ declare global {
42
+ interface HTMLInputElement {
43
+ // Extend HTMLElement to cover input elements
44
+ addEventListener<K extends keyof CustomEventMap>(
45
+ type: K,
46
+ listener: (this: HTMLElement, ev: CustomEventMap[K]) => any,
47
+ options?: boolean | AddEventListenerOptions
48
+ ): void;
49
+ }
50
+ }
51
+
52
+ type BackupSettings = {
53
+ id: string;
54
+ time: string;
55
+ };
56
+
57
+ // TODO
58
+ // - Scroll to newly selected node by path
59
+ // - When rootPath get all the levels and batch them at the root level for better usability
60
+
61
+ @Component({
62
+ selector: 'app-file-tree',
63
+ standalone: true,
64
+ imports: [
65
+ SparkleButtonGroupComponent,
66
+ SparkleToggleComponent,
67
+ SparkleIconComponent,
68
+ SparkleFormFieldComponent,
69
+ SparkleListComponent,
70
+ SparkleCheckboxComponent,
71
+ NgTemplateOutlet,
72
+ JsonPipe,
73
+ FormsModule,
74
+ ],
75
+ templateUrl: './file-tree.component.html',
76
+ styleUrl: './file-tree.component.scss',
77
+ changeDetection: ChangeDetectionStrategy.OnPush,
78
+ host: {
79
+ '[class.disabled]': 'disabled()',
80
+ },
81
+ })
82
+ export default class FileTreeComponent {
83
+ #dupServer = inject(DuplicatiServerService);
84
+
85
+ multiSelect = input(false);
86
+ disabled = input(false);
87
+ showFiles = input(false);
88
+ startingPath = input<string | null>(null);
89
+ rootPath = input<string | undefined>(undefined);
90
+ backupSettings = input<BackupSettings | null>(null);
91
+ includes = output<string[]>();
92
+ excludes = output<string[]>();
93
+
94
+ treeContainerRef = viewChild<ElementRef<HTMLDivElement>>('treeContainer');
95
+ formRef = viewChild<ElementRef<HTMLFormElement>>('formRef');
96
+ pathDiscoveryMethod = signal<'browse' | 'path'>('browse');
97
+ isLoading = signal(false);
98
+ showHiddenFiles = signal(false);
99
+ currentPath = signal<string>('/');
100
+ #inputRef = signal<HTMLInputElement | null>(null);
101
+ treeSearchQuery = signal<string>('');
102
+ treeNodes = signal<TreeNode[]>([]);
103
+
104
+ isByBackupSettings = computed(() => this.rootPath() && this.backupSettings()?.id && this.backupSettings()?.time);
105
+ searchableTreeNodes = computed<TreeNode[]>(() => {
106
+ const query = this.treeSearchQuery();
107
+ return query
108
+ ? this.treeNodes().filter((x) => x.text?.toLowerCase().includes(query.toLowerCase()))
109
+ : this.treeNodes();
110
+ });
111
+ treeStructure = computed<FileTreeNode[]>(() => {
112
+ const nodes = this.searchableTreeNodes();
113
+ const nodeMap = new Map<string, FileTreeNode>();
114
+ const rootPath = this.rootPath();
115
+
116
+ const root: FileTreeNode = rootPath
117
+ ? {
118
+ id: rootPath,
119
+ resolvedpath: rootPath,
120
+ text: rootPath?.replaceAll('/', ''),
121
+ parentPath: '',
122
+ children: [],
123
+ isSelected: false,
124
+ cls: 'folder',
125
+ }
126
+ : {
127
+ id: '/',
128
+ resolvedpath: '/',
129
+ text: 'Computer',
130
+ parentPath: '',
131
+ children: [],
132
+ isSelected: false,
133
+ };
134
+
135
+ nodeMap.set('/', root);
136
+
137
+ for (const node of nodes) {
138
+ const itemPath = (node.id as string).replace(rootPath ?? '', '');
139
+ const pathParts = itemPath.split('/').filter(Boolean);
140
+
141
+ let currentPath = '';
142
+ let parentNode = root;
143
+
144
+ for (const part of pathParts) {
145
+ currentPath += '/' + part;
146
+
147
+ if (!nodeMap.has(currentPath)) {
148
+ const newNode: FileTreeNode = {
149
+ id: node.id,
150
+ isSelected: node.isSelected,
151
+ parentPath: currentPath,
152
+ resolvedpath: node.id?.startsWith('%') ? node.resolvedpath + '/' : currentPath,
153
+ hidden: node.hidden,
154
+ text: node.text,
155
+ cls: node.cls,
156
+ children: [],
157
+ };
158
+ nodeMap.set(currentPath, newNode);
159
+ parentNode.children.push(newNode);
160
+ }
161
+
162
+ parentNode = nodeMap.get(currentPath)!;
163
+ }
164
+ }
165
+
166
+ return [root];
167
+ });
168
+
169
+ pathDiscoveryMethodEffect = effect(
170
+ () => {
171
+ const disvoeryMethod = this.pathDiscoveryMethod();
172
+ const input = this.#inputRef();
173
+
174
+ if (disvoeryMethod === 'browse' && input) {
175
+ if (this.multiSelect()) {
176
+ const arr = input.value.split('\0');
177
+ this.treeNodes.update((y) =>
178
+ y.map((z) => ({ ...z, isSelected: typeof z.id === 'string' && arr.includes(z.id) }))
179
+ );
180
+ } else {
181
+ this.treeNodes.update((y) => y.map((z) => ({ ...z, isSelected: z.id === input.value })));
182
+ }
183
+ }
184
+ },
185
+ {
186
+ allowSignalWrites: true,
187
+ }
188
+ );
189
+
190
+ #scrollToFirstSelectedNode() {
191
+ const selectedNodes = this.treeContainerRef()?.nativeElement.querySelectorAll('.active');
192
+
193
+ if (selectedNodes && selectedNodes.length > 0) {
194
+ selectedNodes[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
195
+ }
196
+ }
197
+
198
+ currentPathEffect = effect(
199
+ () => {
200
+ const input = this.#inputRef();
201
+
202
+ if (input) {
203
+ input.value = this.currentPath();
204
+ input.dispatchEvent(new Event('input'));
205
+ }
206
+ },
207
+ {
208
+ allowSignalWrites: true,
209
+ }
210
+ );
211
+
212
+ selectedPathsEffect = effect(
213
+ () => {
214
+ if (this.multiSelect()) {
215
+ const selectedNodes = this.treeNodes().filter((node) => node.isSelected);
216
+ const excludedNodes = this.treeNodes().filter(
217
+ (node) => !node.isSelected && selectedNodes.some((selected) => node.id?.startsWith(selected.id as string))
218
+ );
219
+
220
+ const selectedMapped = selectedNodes
221
+ .filter(
222
+ (node) =>
223
+ !selectedNodes.some(
224
+ (otherNode) => node.id !== otherNode.id && node.id?.startsWith(otherNode.id as string)
225
+ )
226
+ )
227
+ .map((node) => node.id as string);
228
+
229
+ const excludedMapped = excludedNodes
230
+ .filter(
231
+ (node) =>
232
+ !excludedNodes.some(
233
+ (otherNode) => node.id !== otherNode.id && node.id?.startsWith(otherNode.id as string)
234
+ )
235
+ )
236
+ .map((node) => `-${node.id as string}`);
237
+
238
+ this.excludes.emit(excludedMapped);
239
+
240
+ const newPath = [...selectedMapped, ...excludedMapped] as string[];
241
+
242
+ this.currentPath.set(newPath.join('\0'));
243
+ } else {
244
+ const selectedNode = this.treeNodes().find((node) => node.isSelected);
245
+
246
+ const resolvedPath = selectedNode?.resolvedpath as string;
247
+
248
+ this.currentPath.set(selectedNode?.id ?? '');
249
+ }
250
+ },
251
+ {
252
+ allowSignalWrites: true,
253
+ }
254
+ );
255
+
256
+ inputValueChangeAbortController: AbortController | null = null;
257
+ inputRefEffect = effect(
258
+ () => {
259
+ const input = this.formRef()?.nativeElement?.querySelector('input');
260
+
261
+ if (!input) return;
262
+
263
+ this.#createCustomInputEventListener(input);
264
+
265
+ if (this.inputValueChangeAbortController) {
266
+ this.inputValueChangeAbortController.abort();
267
+ }
268
+
269
+ this.inputValueChangeAbortController = new AbortController();
270
+
271
+ input.addEventListener(
272
+ 'inputValueChanged',
273
+ (event: InputValueChangedEvent) => {
274
+ this.currentPath.set(event.detail.value);
275
+ },
276
+ { signal: this.inputValueChangeAbortController.signal }
277
+ );
278
+
279
+ this.#inputRef.set(input);
280
+ input.autocomplete = 'off';
281
+ },
282
+ { allowSignalWrites: true }
283
+ );
284
+
285
+ ngOnInit() {
286
+ const startingPath = this.startingPath();
287
+
288
+ if (startingPath) {
289
+ this.currentPath.set(startingPath);
290
+ this.#fetchPathSegmentsRecursively(startingPath);
291
+ } else {
292
+ this.#getPath(null, this.rootPath());
293
+ }
294
+ }
295
+
296
+ toggleSelectedNode($event: Event, node: FileTreeNode) {
297
+ $event.stopPropagation();
298
+
299
+ if (this.multiSelect()) {
300
+ this.treeNodes.update((y) =>
301
+ y.map((z) => {
302
+ if (z.id?.startsWith(node.id as string)) {
303
+ return { ...z, isSelected: !node.isSelected };
304
+ }
305
+ return z;
306
+ })
307
+ );
308
+ } else {
309
+ this.treeNodes.update((y) =>
310
+ y.map((z) => {
311
+ if (z.id === (node.id as string)) {
312
+ return { ...z, isSelected: !node.isSelected };
313
+ } else {
314
+ return { ...z, isSelected: false };
315
+ }
316
+ })
317
+ );
318
+ }
319
+ }
320
+
321
+ toggleNode($event: Event, path: string, node: FileTreeNode | null = null) {
322
+ $event.stopPropagation();
323
+
324
+ if (node?.children.length) {
325
+ this.treeNodes.update((y) => y.filter((z) => !z.id?.startsWith(node.id as string) || z.id === node.id));
326
+ } else {
327
+ this.#getPath(node, path);
328
+ }
329
+ }
330
+
331
+ #createCustomInputEventListener(input: HTMLInputElement) {
332
+ Object.defineProperty(input, 'value', {
333
+ configurable: true,
334
+ get() {
335
+ return Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.get!.call(this);
336
+ },
337
+ set(newVal) {
338
+ Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(this, newVal);
339
+
340
+ this.dispatchEvent(
341
+ new CustomEvent('inputValueChanged', {
342
+ bubbles: true,
343
+ cancelable: true,
344
+ detail: {
345
+ value: newVal,
346
+ },
347
+ })
348
+ );
349
+
350
+ return newVal;
351
+ },
352
+ });
353
+
354
+ return input;
355
+ }
356
+
357
+ #getFilesystemPath(path: string) {
358
+ return this.#dupServer.postApiV1Filesystem({
359
+ showHidden: true,
360
+ requestBody: {
361
+ path,
362
+ },
363
+ onlyFolders: !this.showFiles(),
364
+ });
365
+ }
366
+
367
+ #getBackupFiles(path: string | null) {
368
+ const backupSettings = this.backupSettings()!;
369
+ const params: GetApiV1BackupByIdFilesData = {
370
+ id: backupSettings.id,
371
+ time: backupSettings.time,
372
+ prefixOnly: false,
373
+ folderContents: true,
374
+ };
375
+
376
+ if (path) {
377
+ params.filter = '@' + path;
378
+ }
379
+
380
+ return this.#dupServer.getApiV1BackupByIdFiles(params);
381
+ }
382
+
383
+ #getFilePath(path: string) {
384
+ return this.isByBackupSettings() ? this.#getBackupFiles(path) : this.#getFilesystemPath(path);
385
+ }
386
+
387
+ #fetchPathSegmentsRecursively(path: string) {
388
+ const deselectedPaths = path
389
+ .split('\0')
390
+ .filter((x) => x.startsWith('-'))
391
+ .map((x) => x.slice(1, -1));
392
+
393
+ const pathArr = path
394
+ .split('\0')
395
+ .filter((x) => !x.startsWith('-'))
396
+ .filter(Boolean);
397
+ const segmentArr = pathArr.map((x) => x.split('/').filter(Boolean));
398
+
399
+ let urlPieces: string[] = [this.rootPath() ?? '/'];
400
+
401
+ segmentArr.forEach((segments) => {
402
+ segments.forEach((_, index) => {
403
+ const urlPiece = '/' + segments.slice(0, index + 1).join('/');
404
+ if (!urlPieces.includes(urlPiece)) {
405
+ urlPieces.push(urlPiece);
406
+ }
407
+ });
408
+ });
409
+
410
+ this.isLoading.set(true);
411
+
412
+ forkJoin([...urlPieces.map((urlPiece) => this.#getFilePath(urlPiece === '/' ? urlPiece : urlPiece))])
413
+ .pipe(finalize(() => this.isLoading.set(false)))
414
+ .subscribe({
415
+ next: (results) => {
416
+ this.treeNodes.update((y) => {
417
+ const allNewNodes = results.flatMap((x) => {
418
+ return (x as any).map((z: any) => {
419
+ const path = z.id;
420
+ const parentPath = path && path.split('/').filter(Boolean).slice(0, -1).join('/');
421
+ const _pathWithoutTrailingSlash = path && path.split('/').filter(Boolean).join('/');
422
+ const pathWithoutTrailingSlash = '/' + _pathWithoutTrailingSlash;
423
+ const found =
424
+ pathArr.findIndex((x) => {
425
+ return this.multiSelect() ? pathWithoutTrailingSlash.startsWith(x) : pathWithoutTrailingSlash === x;
426
+ }) > -1;
427
+
428
+ const hi = pathArr.findIndex((x) => x === path) > -1;
429
+ const isDeselected = deselectedPaths.findIndex((x) => x === pathWithoutTrailingSlash) > -1;
430
+
431
+ return {
432
+ ...z,
433
+ parentPath: parentPath.startsWith('%') ? parentPath : '/' + parentPath,
434
+ isSelected: (found && !isDeselected) || hi, // TODO - Use deselected paths to disable
435
+ };
436
+ });
437
+ });
438
+
439
+ const arrayUniqueByKey = [
440
+ ...new Map([...y, ...allNewNodes].map((item) => [item.resolvedpath ?? item.id, item])).values(),
441
+ ];
442
+
443
+ return arrayUniqueByKey as TreeNode[];
444
+ });
445
+ },
446
+ });
447
+ }
448
+
449
+ #getPath(node: FileTreeNode | null = null, newPath = '/') {
450
+ this.isLoading.set(true);
451
+
452
+ (this.#getFilePath(newPath) as Observable<any>).pipe(finalize(() => this.isLoading.set(false))).subscribe({
453
+ next: (x) => {
454
+ const alignDataArray = this.isByBackupSettings()
455
+ ? x['Files'].map((y: { Path: string; Size: any }) => {
456
+ return {
457
+ text: y.Path.split('/')
458
+ .filter((part) => part !== '')
459
+ .pop(),
460
+ id: y.Path,
461
+ cls: y.Path.endsWith('/') ? 'folder' : 'file',
462
+ leaf: node !== null,
463
+ resolvedpath: y.Path,
464
+ hidden: false,
465
+ };
466
+ })
467
+ : x;
468
+
469
+ this.treeNodes.update((y) => {
470
+ const newArray = alignDataArray.map((z: any) => ({
471
+ ...z,
472
+ parentPath: newPath,
473
+ isSelected: (this.multiSelect() && node?.isSelected) ?? false,
474
+ }));
475
+
476
+ const arrayUniqueByKey = [
477
+ ...new Map([...y, ...newArray].map((item) => [item.resolvedpath ?? item.id, item])).values(),
478
+ ];
479
+
480
+ return arrayUniqueByKey as TreeNode[];
481
+ });
482
+ },
483
+ });
484
+ }
485
+ }