@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,367 @@
1
+ @if (destinationOptions().length && sysIsLoaded() && destinationIsLoaded()) {
2
+ <form [formGroup]="destinationForm" #formRef (submit)="next()">
3
+ <header>
4
+ <h3 class="title-30">Backup destination</h3>
5
+ @if (destinationCount() > 0) {
6
+ <p (click)="removeDestinationFormGroup(destinationCount() - 1)">
7
+ Wanna select another destination?
8
+ <span class="spk-primary">Click here</span>
9
+ </p>
10
+ }
11
+ </header>
12
+
13
+ @if (destinationCount() === 0) {
14
+ <div class="most-common-destinations">
15
+ <div class="tile" (click)="addDestinationFormGroup('file')">
16
+ <h3>File system</h3>
17
+ <p>Store backups on your local file system.</p>
18
+ </div>
19
+
20
+ <div class="tile" (click)="addDestinationFormGroup('gcs')">
21
+ <h3>Google Cloud Storage</h3>
22
+ <p>Store backups in Google Cloud Storage.</p>
23
+ </div>
24
+
25
+ <div class="tile" (click)="addDestinationFormGroup('s3')">
26
+ <h3>S3 compatible bucket</h3>
27
+ <p>Store backups in any S3 compatible bucket.</p>
28
+ </div>
29
+
30
+ <div class="tile" (click)="addDestinationFormGroup('ssh')">
31
+ <h3>SSH</h3>
32
+ <p>Store backups in SSH.</p>
33
+ </div>
34
+ <div class="tile" (click)="addDestinationFormGroup('googledrive')">
35
+ <h3>Google Drive</h3>
36
+ <p>Store backups in Google Drive.</p>
37
+ </div>
38
+ </div>
39
+
40
+ <p>More destinations are coming soon.</p>
41
+ <!-- <spk-menu>
42
+ <button spk-button type="button" class="outlined">Other destinations</button>
43
+
44
+ <ng-container menu>
45
+ @for (destination of destinationOptions(); track $index) {
46
+ <button spk-menu-item type="button" (click)="addDestinationFormGroup(destination.Key)">
47
+ {{ destination.DisplayName }}
48
+ </button>
49
+ }
50
+ </ng-container>
51
+ </spk-menu> -->
52
+ }
53
+
54
+ @if (destinationFormSignal()?.destinations?.length) {
55
+ <ng-container formArrayName="destinations">
56
+ <ng-container formGroupName="0">
57
+ @if (destinationFormPair().custom.length) {
58
+ <ng-container>
59
+ @for (destination of destinationFormPair().custom; track $index) {
60
+ <ng-container
61
+ *ngTemplateOutlet="
62
+ fieldTemplate;
63
+ context: {
64
+ $implicit: {
65
+ destination: destination,
66
+ destinationType: destinationFormSignal()?.destinations?.[0]?.destinationType,
67
+ index: $index,
68
+ formGroupName: 'custom',
69
+ oauthField: destinationFormPair().oauthField,
70
+ },
71
+ }
72
+ "></ng-container>
73
+ }
74
+ </ng-container>
75
+ }
76
+
77
+ @if (destinationFormPair().dynamic.length) {
78
+ <ng-container>
79
+ @for (destination of destinationFormPair().dynamic; track $index) {
80
+ <ng-container
81
+ *ngTemplateOutlet="
82
+ fieldTemplate;
83
+ context: {
84
+ $implicit: {
85
+ destination: destination,
86
+ destinationType: destinationFormSignal()?.destinations?.[0]?.destinationType,
87
+ index: $index,
88
+ formGroupName: 'dynamic',
89
+ oauthField: destinationFormPair().oauthField,
90
+ },
91
+ }
92
+ "></ng-container>
93
+ }
94
+ </ng-container>
95
+ }
96
+
97
+ @if (destinationFormPair().advanced.length) {
98
+ <app-toggle-card [disallowToggle]="true">
99
+ <ng-container title>
100
+ Advanced options
101
+
102
+ <spk-menu>
103
+ <button spk-button type="button" class="outlined small">
104
+ Add advanced option
105
+ <spk-icon>plus</spk-icon>
106
+ </button>
107
+
108
+ <ng-container menu>
109
+ @for (option of notSelectedAdvancedFormPair(); track $index) {
110
+ <button spk-menu-item type="button" (click)="addAdvancedFormPair(option)">
111
+ {{ option.name }}
112
+ </button>
113
+ }
114
+ </ng-container>
115
+ </spk-menu>
116
+ </ng-container>
117
+
118
+ <ng-container>
119
+ @for (destination of selectedAdvancedFormPair(); track $index) {
120
+ <ng-container
121
+ *ngTemplateOutlet="
122
+ fieldTemplate;
123
+ context: {
124
+ $implicit: {
125
+ destination: destination,
126
+ destinationType: destinationFormSignal()?.destinations?.[0]?.destinationType,
127
+ index: $index,
128
+ formGroupName: 'advanced',
129
+ oauthField: destinationFormPair().oauthField,
130
+ },
131
+ }
132
+ "></ng-container>
133
+ }
134
+ </ng-container>
135
+ </app-toggle-card>
136
+ }
137
+
138
+ <ng-template #fieldTemplate let-item>
139
+ <ng-container [formGroupName]="item.formGroupName">
140
+ @if (item.destination.type === 'file-tree') {
141
+ <app-file-tree
142
+ [startingPath]="getFormFieldValue(item.index, item.formGroupName, item.destination.name)">
143
+ <label for="destination-{{ item.formGroupName }}-{{ item.formGroupName }}-{{ item.index }}">
144
+ {{ item.destination.shortDescription ?? item.destination.name }}
145
+ @if (item.destination.longDescription) {
146
+ <spk-icon [attr.spk-tooltip-primary]="item.destination.longDescription" spk-tooltip-right>
147
+ question
148
+ </spk-icon>
149
+ }
150
+ </label>
151
+ <input
152
+ type="text"
153
+ id="destination-{{ item.formGroupName }}-{{ item.index }}"
154
+ formControlName="{{ item.destination.name }}" />
155
+ </app-file-tree>
156
+ } @else if (item.destination.type === 'String' || item.destination.type === 'Path') {
157
+ <spk-form-field>
158
+ <label for="destination-{{ item.formGroupName }}-{{ item.index }}">
159
+ {{ item.destination.shortDescription ?? item.destination.name }}
160
+ @if (item.destination.longDescription) {
161
+ <spk-icon [attr.spk-tooltip-primary]="item.destination.longDescription" spk-tooltip-right>
162
+ question
163
+ </spk-icon>
164
+ }
165
+ </label>
166
+
167
+ <input
168
+ type="text"
169
+ id="destination-{{ item.formGroupName }}-{{ item.index }}"
170
+ formControlName="{{ item.destination.name }}" />
171
+
172
+ @if (item.destination.type === 'Path') {
173
+ <spk-icon spkPrefix>link</spk-icon>
174
+ } @else {
175
+ <spk-icon spkPrefix>textbox</spk-icon>
176
+ }
177
+ </spk-form-field>
178
+ } @else if (item.destination.type === 'Password') {
179
+ <spk-form-field>
180
+ <label for="destination-{{ item.formGroupName }}-{{ item.index }}">
181
+ {{ item.destination.shortDescription ?? item.destination.name }} -- {{ item.oauthField }}
182
+ {{ item.destinationType }} {{ item.destination.name }}
183
+ </label>
184
+ <input
185
+ [type]="$any(item.destination).showPassword ? 'text' : 'password'"
186
+ id="destination-{{ item.formGroupName }}-{{ item.index }}"
187
+ formControlName="{{ item.destination.name }}" />
188
+
189
+ @if (item.oauthField === item.destination.name) {
190
+ <span spkPrefix class="link" (click)="oauthStartTokenCreation(item.destinationType)">AuthID</span>
191
+ } @else {
192
+ <spk-icon spkPrefix>password</spk-icon>
193
+ }
194
+ @if ($any(item.destination).showPassword) {
195
+ <spk-icon
196
+ class="clickable"
197
+ spkSuffix
198
+ (click)="$any(item.destination).showPassword = !$any(item.destination).showPassword">
199
+ eye
200
+ </spk-icon>
201
+ } @else {
202
+ <spk-icon
203
+ class="clickable"
204
+ spkSuffix
205
+ (click)="$any(item.destination).showPassword = !$any(item.destination).showPassword">
206
+ eye-slash
207
+ </spk-icon>
208
+ }
209
+ </spk-form-field>
210
+ } @else if (item.destination.type === 'Enumeration') {
211
+ <spk-select>
212
+ <label for="destination-{{ item.formGroupName }}-{{ item.index }}">
213
+ {{ item.destination.shortDescription ?? item.destination.name }}
214
+
215
+ @if (item.destination.longDescription) {
216
+ <spk-icon [attr.spk-tooltip-primary]="item.destination.longDescription" spk-tooltip-right>
217
+ question
218
+ </spk-icon>
219
+ }
220
+ </label>
221
+ <input
222
+ type="search"
223
+ id="destination-{{ item.formGroupName }}-{{ item.index }}"
224
+ formControlName="{{ item.destination.name }}" />
225
+
226
+ <ng-container options>
227
+ @for (option of item.destination.options; track $index) {
228
+ <option [value]="option">{{ option }}</option>
229
+ }
230
+ </ng-container>
231
+ </spk-select>
232
+ } @else if (item.destination.type === 'Size') {
233
+ <div class="form-column" [formGroupName]="item.destination.name">
234
+ <label for="destination-{{ item.formGroupName }}-{{ item.index }}">
235
+ {{ item.destination.shortDescription ?? item.destination.name }}
236
+
237
+ @if (item.destination.longDescription) {
238
+ <spk-icon [attr.spk-tooltip-primary]="item.destination.longDescription" spk-tooltip-right>
239
+ question
240
+ </spk-icon>
241
+ }
242
+ </label>
243
+
244
+ <div class="form-row">
245
+ <spk-form-field>
246
+ <input
247
+ type="number"
248
+ id="destination-{{ item.formGroupName }}-{{ item.index }}"
249
+ formControlName="size" />
250
+
251
+ <spk-icon spkPrefix>resize</spk-icon>
252
+ </spk-form-field>
253
+
254
+ <spk-select>
255
+ <label></label>
256
+ <input type="search" formControlName="unit" />
257
+
258
+ <ng-container options>
259
+ @for (option of sizeOptions(); track $index) {
260
+ <option [value]="option">{{ option }}</option>
261
+ }
262
+ </ng-container>
263
+ </spk-select>
264
+ </div>
265
+ </div>
266
+ } @else if (item.destination.type === 'Integer') {
267
+ <spk-form-field>
268
+ <label for="destination-{{ item.formGroupName }}-{{ item.index }}">
269
+ {{ item.destination.shortDescription ?? item.destination.name }}
270
+ @if (item.destination.longDescription) {
271
+ <spk-icon [attr.spk-tooltip-primary]="item.destination.longDescription" spk-tooltip-right>
272
+ question
273
+ </spk-icon>
274
+ }
275
+ </label>
276
+ <input
277
+ type="number"
278
+ id="destination-{{ item.formGroupName }}-{{ item.index }}"
279
+ formControlName="{{ item.destination.name }}" />
280
+ <spk-icon spkPrefix>numpad</spk-icon>
281
+ </spk-form-field>
282
+ } @else if (item.destination.type === 'Boolean') {
283
+ <spk-toggle class="primary raised">
284
+ <label for="destination-{{ item.formGroupName }}-{{ item.index }}">
285
+ {{ item.destination.shortDescription ?? item.destination.name }}
286
+ @if (item.destination.longDescription) {
287
+ <spk-icon [attr.spk-tooltip-primary]="item.destination.longDescription" spk-tooltip-right>
288
+ question
289
+ </spk-icon>
290
+ }
291
+ </label>
292
+ <input
293
+ type="checkbox"
294
+ id="destination-{{ item.formGroupName }}-{{ item.index }}"
295
+ formControlName="{{ item.destination.name }}" />
296
+ </spk-toggle>
297
+ } @else if (item.destination.type === 'Flags') {
298
+ <spk-select [selectMultiple]="true" class="primary">
299
+ <label for="destination-{{ item.formGroupName }}-{{ item.index }}">
300
+ {{ item.destination.shortDescription ?? item.destination.name }}
301
+ @if (item.destination.longDescription) {
302
+ <spk-icon [attr.spk-tooltip-primary]="item.destination.longDescription" spk-tooltip-right>
303
+ question
304
+ </spk-icon>
305
+ }
306
+ </label>
307
+ <input type="text" id="text" formControlName="{{ item.destination.name }}" />
308
+
309
+ <ng-container options>
310
+ @for (option of item.destination.options; track $index) {
311
+ @if (option === 'None') {
312
+ <spk-option [value]="'None'" deselect>None</spk-option>
313
+ } @else {
314
+ <spk-option [value]="option">
315
+ {{ option }}
316
+
317
+ <spk-checkbox class="raised primary" />
318
+ </spk-option>
319
+ }
320
+ }
321
+ </ng-container>
322
+ </spk-select>
323
+ } @else if (item.destination.type === 'Timespan') {
324
+ <spk-form-field>
325
+ <label for="destination-{{ item.formGroupName }}-{{ item.index }}">
326
+ {{ item.destination.shortDescription ?? item.destination.name }}
327
+ @if (item.destination.longDescription) {
328
+ <spk-icon [attr.spk-tooltip-primary]="item.destination.longDescription" spk-tooltip-right>
329
+ question
330
+ </spk-icon>
331
+ }
332
+ </label>
333
+ <input
334
+ type="text"
335
+ id="destination-{{ item.formGroupName }}-{{ item.index }}"
336
+ formControlName="{{ item.destination.name }}" />
337
+
338
+ <span spkError>
339
+ <!-- {{ destinationForm.controls.destinations.controls[index]?.hasError('pattern') }} -->
340
+ </span>
341
+ </spk-form-field>
342
+ }
343
+ </ng-container>
344
+ </ng-template>
345
+ </ng-container>
346
+ </ng-container>
347
+ }
348
+
349
+ <div class="form-actions">
350
+ <button spk-button type="button" (click)="goBack()">
351
+ <spk-icon>arrow-left</spk-icon>
352
+ Go back
353
+ </button>
354
+
355
+ <button spk-button class="raised primary" type="submit">
356
+ <spk-icon>arrow-right</spk-icon>
357
+ Continue
358
+ </button>
359
+ </div>
360
+ </form>
361
+
362
+ <!-- <pre>{{ destinationFormSignal() | json }}</pre> -->
363
+ <!-- <pre style="white-space: wrap">{{ targetUrl() }}</pre>
364
+ <pre>{{ destinationFormSignal() | json }}</pre> -->
365
+ <!-- <pre>{{ targetUrlsAsFormObjects() | json }}</pre>
366
+ <pre>{{ formObjectsAsTargetUrls() | json }}</pre> -->
367
+ }
@@ -0,0 +1,85 @@
1
+ @use '@sparkle-ui/core/styles/helpers' as *;
2
+
3
+ :host {
4
+ display: block;
5
+ }
6
+
7
+ header {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: p2r(8);
11
+
12
+ p {
13
+ span {
14
+ cursor: pointer;
15
+ text-decoration: underline;
16
+ }
17
+ }
18
+ }
19
+
20
+ form {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: p2r(24);
24
+ }
25
+
26
+ .form-actions {
27
+ display: flex;
28
+ justify-content: space-between;
29
+ }
30
+
31
+ .form-column {
32
+ display: flex;
33
+ flex-direction: column;
34
+
35
+ label {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: p2r(4);
39
+ }
40
+
41
+ spk-icon {
42
+ font-size: p2r(16);
43
+ }
44
+ }
45
+
46
+ .form-row {
47
+ display: grid;
48
+ grid-template-columns: 2fr 1fr;
49
+ gap: p2r(8);
50
+ align-items: flex-end;
51
+ }
52
+
53
+ .clickable {
54
+ cursor: pointer;
55
+ color: var(--primary-500);
56
+ }
57
+
58
+ .most-common-destinations {
59
+ display: grid;
60
+ grid-template-columns: 1fr 1fr;
61
+ gap: p2r(16);
62
+
63
+ .tile {
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: p2r(8);
67
+ padding: p2r(16);
68
+ border: 1px solid var(--base-level-40);
69
+ border-radius: var(--shape-3);
70
+ box-shadow: var(--shadow-1);
71
+ background-color: var(--base-level-20);
72
+ cursor: pointer;
73
+
74
+ h3 {
75
+ font: var(--paragraph-10);
76
+ margin: 0;
77
+ }
78
+ }
79
+ }
80
+
81
+ [spkprefix].link {
82
+ cursor: pointer;
83
+ text-decoration: underline;
84
+ color: var(--primary-500);
85
+ }
@@ -0,0 +1,249 @@
1
+ import { JsonPipe, NgTemplateOutlet } from '@angular/common';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, signal, viewChild } from '@angular/core';
4
+ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
5
+ import { ActivatedRoute, Router } from '@angular/router';
6
+ import {
7
+ SparkleButtonGroupComponent,
8
+ SparkleCheckboxComponent,
9
+ SparkleDividerComponent,
10
+ SparkleFormFieldComponent,
11
+ SparkleIconComponent,
12
+ SparkleMenuComponent,
13
+ SparkleOptionComponent,
14
+ SparkleSelectComponent,
15
+ SparkleToggleComponent,
16
+ } from '@sparkle-ui/core';
17
+ import { finalize } from 'rxjs';
18
+ import FileTreeComponent from '../../core/components/file-tree/file-tree.component';
19
+ import ToggleCardComponent from '../../core/components/toggle-card/toggle-card.component';
20
+ import { IDynamicModule } from '../../core/openapi';
21
+ import { SysinfoState } from '../../core/states/sysinfo.state';
22
+ import { CreateEditBackupState } from '../create-edit-backup.state';
23
+ import { FormView } from './destination.config';
24
+ import { toTargetPath } from './destination.mapper';
25
+
26
+ const SIZE_OPTIONS = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'] as const;
27
+
28
+ export type Size = (typeof SIZE_OPTIONS)[number];
29
+ const fb = new FormBuilder();
30
+
31
+ export const createDestinationForm = (
32
+ defaults = {
33
+ destinations: [],
34
+ }
35
+ ) => {
36
+ return fb.group({
37
+ destinations: fb.array<DestinationFormGroup>(defaults.destinations),
38
+ });
39
+ };
40
+
41
+ export const createDestinationFormGroup = ({
42
+ key,
43
+ customGroup,
44
+ dynamicGroup,
45
+ advancedGroup,
46
+ }: {
47
+ key: string;
48
+ customGroup: FormGroup;
49
+ dynamicGroup: FormGroup;
50
+ advancedGroup: FormGroup;
51
+ }) => {
52
+ return fb.group({
53
+ destinationType: fb.control<string>(key),
54
+ custom: customGroup,
55
+ dynamic: dynamicGroup,
56
+ advanced: advancedGroup,
57
+ });
58
+ };
59
+
60
+ export type DestinationFormGroup = ReturnType<typeof createDestinationFormGroup>;
61
+ export type DestinationFormGroupValue = ReturnType<typeof createDestinationFormGroup>['value'];
62
+
63
+ @Component({
64
+ selector: 'app-destination',
65
+ standalone: true,
66
+ imports: [
67
+ ReactiveFormsModule,
68
+ NgTemplateOutlet,
69
+
70
+ SparkleFormFieldComponent,
71
+ SparkleSelectComponent,
72
+ SparkleMenuComponent,
73
+ SparkleCheckboxComponent,
74
+ SparkleOptionComponent,
75
+ SparkleButtonGroupComponent,
76
+ SparkleIconComponent,
77
+ SparkleToggleComponent,
78
+ SparkleDividerComponent,
79
+
80
+ ToggleCardComponent,
81
+ FileTreeComponent,
82
+ JsonPipe,
83
+ ],
84
+ templateUrl: './destination.component.html',
85
+ styleUrl: './destination.component.scss',
86
+ changeDetection: ChangeDetectionStrategy.OnPush,
87
+ })
88
+ export default class DestinationComponent {
89
+ #router = inject(Router);
90
+ #route = inject(ActivatedRoute);
91
+ #sysinfo = inject(SysinfoState);
92
+ #httpClient = inject(HttpClient);
93
+ #createEditBackupState = inject(CreateEditBackupState);
94
+
95
+ formRef = viewChild.required<ElementRef<HTMLFormElement>>('formRef');
96
+
97
+ sysIsLoaded = this.#sysinfo.isLoaded;
98
+ destinationIsLoaded = this.#createEditBackupState.destinationIsLoaded;
99
+ destinationForm = this.#createEditBackupState.destinationForm;
100
+ destinationFormPair = this.#createEditBackupState.destinationFormPair;
101
+ selectedAdvancedFormPair = this.#createEditBackupState.selectedAdvancedFormPair;
102
+ notSelectedAdvancedFormPair = this.#createEditBackupState.notSelectedAdvancedFormPair;
103
+ destinationOptions = this.#createEditBackupState.destinationOptions;
104
+ destinationFormSignal = this.#createEditBackupState.destinationFormSignal;
105
+ destinationCount = computed(() => this.destinationFormSignal()?.destinations?.length ?? 0);
106
+ sizeOptions = signal(SIZE_OPTIONS);
107
+
108
+ getFormFieldValue(
109
+ destinationIndex: number,
110
+ formGroupName: 'custom' | 'dynamic' | 'advanced',
111
+ formControlName: string
112
+ ) {
113
+ const dest = this.destinationForm.controls.destinations.controls?.[destinationIndex];
114
+ const group = dest.controls?.[formGroupName];
115
+
116
+ return group.controls[formControlName].value;
117
+ }
118
+
119
+ addDestinationFormGroup(key: IDynamicModule['Key']) {
120
+ this.#createEditBackupState.addDestinationFormGroup(key);
121
+ }
122
+
123
+ addAdvancedFormPair(item: FormView) {
124
+ this.selectedAdvancedFormPair.update((y) => {
125
+ y.push(item);
126
+
127
+ return y;
128
+ });
129
+
130
+ this.notSelectedAdvancedFormPair.update((y) => {
131
+ y = y.filter((x) => x.name !== item.name);
132
+
133
+ return y;
134
+ });
135
+ }
136
+
137
+ targetUrl = computed(() => {
138
+ const destinationGroup = this.destinationFormSignal()?.destinations?.[0];
139
+
140
+ if (!destinationGroup) return '';
141
+
142
+ return toTargetPath(destinationGroup);
143
+ });
144
+ mapToTargetUrl(destinationGroup: DestinationFormGroup) {
145
+ return toTargetPath(destinationGroup.value);
146
+ }
147
+
148
+ removeDestinationFormGroup(index: number) {
149
+ this.destinationForm.controls.destinations.removeAt(index);
150
+ }
151
+
152
+ displayFn(key: IDynamicModule['Key']) {
153
+ const item = this.destinationOptions().find((x) => x.Key === key);
154
+
155
+ return item ? `${item.DisplayName}` : '';
156
+ }
157
+
158
+ goBack() {
159
+ this.#router.navigate(['general'], { relativeTo: this.#route.parent });
160
+ }
161
+
162
+ next() {
163
+ this.#router.navigate(['source-data'], { relativeTo: this.#route.parent });
164
+ }
165
+
166
+ #oauthServiceLink = signal('https://duplicati-oauth-handler.appspot.com/').asReadonly();
167
+ #oauthCreateToken = signal(
168
+ Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)
169
+ ).asReadonly();
170
+ #oauthInProgress = signal(false);
171
+ #oauthId = signal(null);
172
+
173
+ keyCreation() {}
174
+
175
+ oauthStartTokenCreation(backendKey: string) {
176
+ // if (!servicelink.endsWith('/')) servicelink += '/';
177
+
178
+ this.#oauthInProgress.set(true);
179
+
180
+ const w = 450;
181
+ const h = 600;
182
+ const startlink = this.#oauthServiceLink() + '?type=' + backendKey + '&token=' + this.#oauthCreateToken();
183
+
184
+ // const countDown = 100;
185
+ // const ft = oauthCreateToken;
186
+ const left = screen.width / 2 - w / 2;
187
+ const top = screen.height / 2 - h / 2;
188
+ const wnd = window.open(
189
+ startlink,
190
+ '_blank',
191
+ 'height=' + h + ',width=' + w + ',menubar=0,status=0,titlebar=0,toolbar=0,left=' + left + ',top=' + top
192
+ );
193
+
194
+ wnd?.addEventListener('blur', (event) => {
195
+ console.log('event blur', event);
196
+ });
197
+
198
+ wnd?.addEventListener('beforeunload', (event) => {
199
+ console.log('event beforeunload', event);
200
+ this.#httpClient
201
+ .get(this.#oauthServiceLink() + 'fetch?callback=JSON_CALLBACK', {
202
+ params: { token: this.#oauthCreateToken() },
203
+ })
204
+ .pipe(finalize(() => this.#oauthInProgress.set(false)))
205
+ .subscribe({
206
+ next: (res: any) => {
207
+ console.log('res', res);
208
+
209
+ if (res?.authid) {
210
+ this.#oauthId.set(res.authid);
211
+
212
+ // wnd.close();
213
+ }
214
+ },
215
+ });
216
+ });
217
+
218
+ // var recheck = function () {
219
+ // countDown--;
220
+ // if (countDown > 0 && ft == oauthCreateToken) {
221
+ // $http
222
+ // .jsonp(servicelink + "fetch?callback=JSON_CALLBACK", {
223
+ // params: { token: ft },
224
+ // })
225
+ // .then(
226
+ // function (response) {
227
+ // if (response.data.authid) {
228
+ // const AuthID = response.data.authid;
229
+ // const oauth_in_progress = false;
230
+ // wnd.close();
231
+ // } else {
232
+ // setTimeout(recheck, 3000);
233
+ // }
234
+ // },
235
+ // function (response) {
236
+ // setTimeout(recheck, 3000);
237
+ // }
238
+ // );
239
+ // } else {
240
+ // const oauth_in_progress = false;
241
+ // if (wnd != null) wnd.close();
242
+ // }
243
+ // };
244
+
245
+ // setTimeout(recheck, 6000);
246
+
247
+ // return false;
248
+ }
249
+ }