@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,103 @@
1
+ import { FormBuilder } from '@angular/forms';
2
+ import { ArgumentType, ICommandLineArgument } from '../../core/openapi';
3
+
4
+ export type FormView = {
5
+ name: string;
6
+ type: ArgumentType | 'file-tree';
7
+ shortDescription?: string;
8
+ longDescription?: string;
9
+ options?: ICommandLineArgument['ValidValues'];
10
+ };
11
+
12
+ export type CustomFormView = FormView & {
13
+ formElement: any;
14
+ };
15
+
16
+ export type DestinationConfig = {
17
+ [key: string]: {
18
+ oauthField?: string;
19
+ customFields?: {
20
+ [key: string]: CustomFormView;
21
+ };
22
+ dynamicFields?: string[];
23
+ advancedFields?: string[];
24
+ };
25
+ };
26
+
27
+ const fb = new FormBuilder();
28
+
29
+ export const DESTINATION_CONFIG: DestinationConfig = {
30
+ file: {
31
+ customFields: {
32
+ path: {
33
+ type: 'file-tree',
34
+ name: 'path',
35
+ shortDescription: 'File path',
36
+ longDescription: 'The path to store the backup',
37
+ formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
38
+ },
39
+ },
40
+ },
41
+ ssh: {
42
+ customFields: {
43
+ server: {
44
+ type: 'String',
45
+ name: 'server',
46
+ shortDescription: 'Server',
47
+ longDescription: 'The server to connect to',
48
+ formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
49
+ },
50
+ port: {
51
+ type: 'Integer',
52
+ name: 'port',
53
+ shortDescription: 'Port',
54
+ longDescription: 'The port to connect to',
55
+ formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
56
+ },
57
+ path: {
58
+ type: 'Path',
59
+ name: 'path',
60
+ shortDescription: 'File path on the server',
61
+ longDescription: 'File path on the server',
62
+ formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
63
+ },
64
+ },
65
+ dynamicFields: ['auth-username'],
66
+ },
67
+ s3: {
68
+ customFields: {
69
+ path: {
70
+ type: 'Path',
71
+ name: 'path',
72
+ shortDescription: 'File path on the server',
73
+ longDescription: 'File path on the server',
74
+ formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
75
+ },
76
+ bucket: {
77
+ type: 'String',
78
+ name: 'bucket',
79
+ shortDescription: 'Bucket name',
80
+ longDescription: 'Bucket name',
81
+ formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
82
+ },
83
+ },
84
+ dynamicFields: ['use-ssl', 's3-server-name', 'auth-username', 'auth-password'],
85
+ },
86
+ gcs: {
87
+ dynamicFields: ['gcs-location', 'gcs-storage-class', 'authid'],
88
+ },
89
+ googledrive: {
90
+ oauthField: 'authid',
91
+
92
+ customFields: {
93
+ path: {
94
+ type: 'Path',
95
+ name: 'path',
96
+ shortDescription: 'File path on the server',
97
+ longDescription: 'File path on the server',
98
+ formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
99
+ },
100
+ },
101
+ dynamicFields: ['authid'],
102
+ },
103
+ };
@@ -0,0 +1,177 @@
1
+ import { FormControl, FormGroup } from '@angular/forms';
2
+ import { DestinationFormGroupValue } from './destination.component';
3
+ import { DESTINATION_CONFIG } from './destination.config';
4
+
5
+ type DestinationType = keyof typeof mappings | string;
6
+
7
+ export type DestinationFormGroup = FormGroup<{
8
+ destinationType: FormControl<string>;
9
+ custom: FormGroup<any>;
10
+ dynamic: FormGroup<any>;
11
+ advanced: FormGroup<any>;
12
+ }>;
13
+
14
+ export type ValueOfDestinationFormGroup = DestinationFormGroup['value'];
15
+
16
+ function handleSearchParams(destinationType: string, urlObj: URL) {
17
+ const advanced: { [key: string]: any } = {};
18
+ const dynamic: { [key: string]: any } = {};
19
+
20
+ urlObj.searchParams.forEach((value, key) => {
21
+ if (DESTINATION_CONFIG[destinationType].dynamicFields?.includes(key)) {
22
+ dynamic[key] = decodeURIComponent(value);
23
+ } else {
24
+ advanced[key] = decodeURIComponent(value);
25
+ }
26
+ });
27
+
28
+ return {
29
+ advanced,
30
+ dynamic,
31
+ };
32
+ }
33
+
34
+ function toSearchParams(arr: [string, string | number | unknown][]) {
35
+ const searchParamString = arr
36
+ .map(([key, value]) => {
37
+ return `${key}=${value ? encodeURIComponent((value as string | number).toString()) : ''}`;
38
+ })
39
+ .join('&');
40
+
41
+ return searchParamString ? '?' + searchParamString : '';
42
+ }
43
+
44
+ const mappings = {
45
+ file: {
46
+ to: (fields: ValueOfDestinationFormGroup): string => {
47
+ const urlParams = toSearchParams([...Object.entries(fields.advanced), ...Object.entries(fields.dynamic)]);
48
+
49
+ return `${fields.destinationType}://${fields.custom.path}${urlParams}`;
50
+ },
51
+ from: (destinationType: string, urlObj: URL, plainPath: string) => {
52
+ const hasLeadingSlash = plainPath.indexOf('file:///') === 0;
53
+ const hostNameCapitalFirstLetter =
54
+ plainPath.split(hasLeadingSlash ? '///' : '//')[1].substring(0, 1) + urlObj.hostname.substring(1);
55
+
56
+ return <ValueOfDestinationFormGroup>{
57
+ destinationType,
58
+ custom: {
59
+ path: `${hasLeadingSlash ? '/' : ''}${hostNameCapitalFirstLetter}${urlObj.pathname}`,
60
+ },
61
+ ...handleSearchParams(destinationType, urlObj),
62
+ };
63
+ },
64
+ },
65
+ ssh: {
66
+ to: (fields: any): string => {
67
+ const urlParams = toSearchParams([...Object.entries(fields.advanced), ...Object.entries(fields.dynamic)]);
68
+
69
+ return `${fields.destinationType}://${fields.custom.server}${fields.custom.port ? ':' + fields.custom.port : ''}${fields.custom.path.startsWith('/') ? fields.custom.path : '/' + fields.custom.path}${urlParams}`;
70
+ },
71
+ from: (destinationType: string, urlObj: URL, plainPath: string) => {
72
+ return <ValueOfDestinationFormGroup>{
73
+ destinationType,
74
+ custom: {
75
+ server: urlObj.hostname,
76
+ port: urlObj.port,
77
+ path: urlObj.pathname,
78
+ },
79
+ ...handleSearchParams(destinationType, urlObj),
80
+ };
81
+ },
82
+ },
83
+ s3: {
84
+ to: (fields: any): string => {
85
+ const urlParams = toSearchParams([...Object.entries(fields.advanced), ...Object.entries(fields.dynamic)]);
86
+
87
+ return `${fields.destinationType}://${fields.custom.bucket}${fields.custom.path}${urlParams}`;
88
+ },
89
+ from: (destinationType: string, urlObj: URL, plainPath: string) => {
90
+ return <ValueOfDestinationFormGroup>{
91
+ destinationType,
92
+ custom: {
93
+ path: urlObj.pathname,
94
+ bucket: urlObj.hostname,
95
+ },
96
+ ...handleSearchParams(destinationType, urlObj),
97
+ };
98
+ },
99
+ },
100
+ gcs: {
101
+ to: (fields: any): string => {
102
+ const bucket = fields.dynamic['gcs-location'] ?? '';
103
+
104
+ delete fields.dynamic['gcs-location'];
105
+
106
+ const urlParams = toSearchParams([...Object.entries(fields.advanced), ...Object.entries(fields.dynamic)]);
107
+
108
+ return `${fields.destinationType}://${bucket}${urlParams}`;
109
+ },
110
+ from: (destinationType: string, urlObj: URL, plainPath: string) => {
111
+ const { advanced, dynamic } = handleSearchParams(destinationType, urlObj);
112
+ return <ValueOfDestinationFormGroup>{
113
+ destinationType,
114
+ advanced,
115
+ dynamic: {
116
+ ...dynamic,
117
+ 'gcs-location': urlObj.hostname + urlObj.pathname,
118
+ },
119
+ };
120
+ },
121
+ },
122
+ googledrive: {
123
+ to: (fields: any): string => {
124
+ const path = fields.custom.path;
125
+
126
+ return `${fields.destinationType}://${path}`;
127
+ },
128
+ from: (destinationType: string, urlObj: URL, plainPath: string) => {
129
+ return <ValueOfDestinationFormGroup>{
130
+ destinationType,
131
+ custom: {
132
+ path: plainPath,
133
+ },
134
+ ...handleSearchParams(destinationType, urlObj),
135
+ };
136
+ },
137
+ },
138
+
139
+ fallbackMapper: {
140
+ to: (fields: any): string => {
141
+ return '';
142
+ },
143
+ from: (destinationType: string, urlObj: URL, plainPath: string) => {
144
+ return <ValueOfDestinationFormGroup>{
145
+ destinationType,
146
+ ...handleSearchParams(destinationType, urlObj),
147
+ };
148
+ },
149
+ },
150
+ };
151
+
152
+ export function toTargetPath(fields: DestinationFormGroupValue): string {
153
+ const destinationType = fields.destinationType;
154
+
155
+ if (!destinationType || !mappings.hasOwnProperty(destinationType)) {
156
+ return ((mappings as any)['fallbackMapper'].to(fields) as string) ?? '';
157
+ }
158
+
159
+ return ((mappings as any)[destinationType].to(fields) as string) ?? '';
160
+ }
161
+
162
+ export function fromTargetPath(targetPath: string) {
163
+ const canParse = URL.canParse(targetPath);
164
+ const destinationType = targetPath.split('://')[0];
165
+ const fakeProtocolPrefixed = 'http://' + targetPath.split('://')[1];
166
+ const urlObj = new URL(fakeProtocolPrefixed);
167
+
168
+ if (!mappings.hasOwnProperty(destinationType)) {
169
+ return (mappings as any)['fallbackMapper'].from(destinationType, urlObj);
170
+ }
171
+
172
+ if (!canParse) {
173
+ throw new Error('Invalid target path');
174
+ }
175
+
176
+ return (mappings as any)[destinationType].from(destinationType, urlObj, targetPath);
177
+ }
@@ -0,0 +1,108 @@
1
+ <form [formGroup]="generalForm" autocomplete="off" #formRef (submit)="next()">
2
+ <h3 class="title-30">General backup settings</h3>
3
+
4
+ <spk-form-field>
5
+ <label for="backupName">Backup name</label>
6
+ <input type="text" tabindex="1" placeholder="Your backup name" formControlName="name" />
7
+ </spk-form-field>
8
+
9
+ <spk-form-field>
10
+ <label for="backupDescription">Backup description</label>
11
+ <textarea
12
+ id="backupDescription"
13
+ tabindex="2"
14
+ placeholder="Your backup description"
15
+ formControlName="description"></textarea>
16
+ </spk-form-field>
17
+
18
+ @if (isNew()) {
19
+ <spk-select [displayValue]="displayFn(generalForm.value.encryption ?? '')">
20
+ <label for="encryption">Encryption</label>
21
+ <input type="text" id="encryption" tabindex="3" placeholder="Encryption" formControlName="encryption" />
22
+
23
+ <ng-container options>
24
+ @for (encryption of encryptionOptions(); track $index) {
25
+ <option [value]="encryption.Key">{{ encryption.DisplayName }}</option>
26
+ }
27
+ </ng-container>
28
+ </spk-select>
29
+
30
+ @if (encryptionFieldSignal() !== 'none') {
31
+ <div class="passwords">
32
+ <spk-form-field>
33
+ <label for="password">Create a password</label>
34
+ <input
35
+ [type]="showPassword() ? 'text' : 'password'"
36
+ id="password"
37
+ tabindex="4"
38
+ placeholder="Password"
39
+ formControlName="password" />
40
+ </spk-form-field>
41
+
42
+ <spk-form-field>
43
+ <input
44
+ [type]="showPassword() ? 'text' : 'password'"
45
+ id="repeatPassword"
46
+ tabindex="5"
47
+ placeholder="Repeat password"
48
+ formControlName="repeatPassword" />
49
+ </spk-form-field>
50
+
51
+ <div class="password-actions">
52
+ <button spk-button type="button" class="small" (click)="showPassword.set(!showPassword())">
53
+ @if (showPassword()) {
54
+ <spk-icon>eye-slash</spk-icon>
55
+ hide
56
+ } @else {
57
+ <spk-icon>eye</spk-icon>
58
+ show
59
+ }
60
+ </button>
61
+
62
+ <button spk-button type="button" class="small" (click)="generatePassword()">
63
+ <spk-icon>password</spk-icon>
64
+ Generate password
65
+ </button>
66
+
67
+ @if (showCopyPassword()) {
68
+ <button spk-button type="button" class="small copy" (click)="copyPassword()">
69
+ @if (copiedPassword()) {
70
+ <spk-icon class="spk-success">check</spk-icon>
71
+ } @else {
72
+ <spk-icon>copy</spk-icon>
73
+ }
74
+ {{ copiedPassword() ? 'Copied' : 'Copy' }} password
75
+ </button>
76
+ }
77
+ </div>
78
+ </div>
79
+ }
80
+ }
81
+
82
+ <div class="form-actions">
83
+ <button spk-button (click)="exit()">
84
+ <spk-icon>arrow-left</spk-icon>
85
+ Exit
86
+ </button>
87
+
88
+ <div
89
+ [attr.spk-tooltip-primary]="
90
+ (generalForm.valid && encryptionFieldSignal() === 'none') || copiedPassword()
91
+ ? undefined
92
+ : 'Remember to copy the password'
93
+ ">
94
+ <button
95
+ spk-button
96
+ class="raised primary"
97
+ type="submit"
98
+ [disabled]="
99
+ generalForm.invalid || (encryptionFieldSignal() !== 'none' && showCopyPassword() && !copiedPassword())
100
+ ">
101
+ <spk-icon>arrow-right</spk-icon>
102
+ Continue
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </form>
107
+
108
+ <!-- <pre>{{ generalForm.value | json }}</pre> -->
@@ -0,0 +1,34 @@
1
+ @use '@sparkle-ui/core/styles/helpers' as *;
2
+
3
+ :host {
4
+ display: block;
5
+ }
6
+
7
+ form {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: p2r(24);
11
+ }
12
+
13
+ .passwords {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: p2r(8);
17
+ }
18
+
19
+ .password-actions {
20
+ display: flex;
21
+ gap: p2r(8);
22
+ align-items: center;
23
+ }
24
+
25
+ .form-actions {
26
+ display: flex;
27
+ justify-content: space-between;
28
+ }
29
+
30
+ .copy {
31
+ spk-icon.spk-success {
32
+ color: var(--success-500);
33
+ }
34
+ }
@@ -0,0 +1,136 @@
1
+ import { JsonPipe } from '@angular/common';
2
+ import { ChangeDetectionStrategy, Component, ElementRef, inject, signal, viewChild } from '@angular/core';
3
+ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
4
+ import { ActivatedRoute, Router } from '@angular/router';
5
+ import {
6
+ SparkleButtonComponent,
7
+ SparkleFormFieldComponent,
8
+ SparkleIconComponent,
9
+ SparkleSelectComponent,
10
+ } from '@sparkle-ui/core';
11
+ import { IDynamicModule } from '../../core/openapi';
12
+ import { PasswordGeneratorService } from '../../core/services/password-generator.service';
13
+ import { SysinfoState } from '../../core/states/sysinfo.state';
14
+ import { validateWhen, watchField } from '../../core/validators/custom.validators';
15
+ import { CreateEditBackupState } from '../create-edit-backup.state';
16
+
17
+ const fb = new FormBuilder();
18
+
19
+ export const createGeneralForm = (
20
+ defaults = {
21
+ name: '',
22
+ description: '',
23
+ encryption: 'none',
24
+ password: '',
25
+ repeatPassword: '',
26
+ }
27
+ ) => {
28
+ return fb.group({
29
+ name: fb.control<string>(defaults.name, [Validators.required]),
30
+ description: fb.control<string>(defaults.description),
31
+ encryption: fb.control<string>(defaults.encryption, [Validators.required, watchField()]),
32
+ password: fb.control<string>(defaults.password, [
33
+ validateWhen((t) => t?.value.encryption !== 'none', [Validators.required]),
34
+ ]),
35
+ repeatPassword: fb.control<string>(defaults.repeatPassword, [
36
+ validateWhen((t) => t?.value.encryption !== 'none', [Validators.required]),
37
+ ]),
38
+ });
39
+ };
40
+
41
+ export const NONE_OPTION = {
42
+ Key: 'none',
43
+ DisplayName: 'None',
44
+ };
45
+
46
+ @Component({
47
+ selector: 'app-general',
48
+ standalone: true,
49
+ imports: [
50
+ ReactiveFormsModule,
51
+ SparkleFormFieldComponent,
52
+ SparkleSelectComponent,
53
+ SparkleButtonComponent,
54
+ SparkleIconComponent,
55
+ JsonPipe,
56
+ ],
57
+ templateUrl: './general.component.html',
58
+ styleUrl: './general.component.scss',
59
+ changeDetection: ChangeDetectionStrategy.OnPush,
60
+ })
61
+ export default class GeneralComponent {
62
+ #router = inject(Router);
63
+ #route = inject(ActivatedRoute);
64
+ #passwordGeneratorService = inject(PasswordGeneratorService);
65
+ #createEditBackupState = inject(CreateEditBackupState);
66
+ #sysinfo = inject(SysinfoState);
67
+
68
+ formRef = viewChild.required<ElementRef<HTMLFormElement>>('formRef');
69
+ generalForm = this.#createEditBackupState.generalForm;
70
+ encryptionFieldSignal = this.#createEditBackupState.encryptionFieldSignal;
71
+ encryptionOptions = this.#createEditBackupState.encryptionOptions;
72
+ isNew = this.#createEditBackupState.isNew;
73
+
74
+ showPassword = signal(false);
75
+ copiedPassword = signal(false);
76
+ showCopyPassword = signal(false);
77
+
78
+ ngOnInit() {
79
+ this.formRef().nativeElement.focus();
80
+ }
81
+
82
+ exit() {
83
+ this.#createEditBackupState.exit();
84
+ }
85
+
86
+ async copyPassword() {
87
+ const pass = this.generalForm.controls.password.value;
88
+
89
+ await this.#copyToClipboard(pass ?? '');
90
+
91
+ this.copiedPassword.set(true);
92
+ }
93
+
94
+ async #copyToClipboard(text: string) {
95
+ try {
96
+ // Attempt to use the Clipboard API
97
+ await navigator.clipboard.writeText(text);
98
+ } catch (err) {
99
+ // Fallback to execCommand if Clipboard API fails
100
+ try {
101
+ const textArea = document.createElement('textarea');
102
+ textArea.value = text;
103
+ document.body.appendChild(textArea);
104
+ textArea.select();
105
+ document.execCommand('copy');
106
+ document.body.removeChild(textArea);
107
+ } catch (err) {
108
+ console.error('Failed to copy text: ', err);
109
+ }
110
+ }
111
+ }
112
+
113
+ displayFn(val: IDynamicModule['Key']) {
114
+ const encryptionOptions = this.#sysinfo.systemInfo()?.EncryptionModules ?? [];
115
+ const item = [NONE_OPTION, ...encryptionOptions].find((x) => x.Key === val);
116
+
117
+ if (!item) {
118
+ return '';
119
+ }
120
+ return `${item.DisplayName}`;
121
+ }
122
+
123
+ generatePassword() {
124
+ this.copiedPassword.set(false);
125
+
126
+ const newPass = this.#passwordGeneratorService.generate(16);
127
+ this.generalForm.controls.password.setValue(newPass);
128
+ this.generalForm.controls.repeatPassword.setValue(newPass);
129
+
130
+ this.showCopyPassword.set(true);
131
+ }
132
+
133
+ next() {
134
+ this.#router.navigate(['destination'], { relativeTo: this.#route.parent });
135
+ }
136
+ }
@@ -0,0 +1,95 @@
1
+ <form [formGroup]="optionsForm" #formRef (submit)="submit()">
2
+ <h3 class="title-30">Options</h3>
3
+
4
+ <div class="remote-volume-wrap">
5
+ <div class="row-inputs" formGroupName="remoteVolumeSize">
6
+ <spk-form-field>
7
+ <label for="size">Remote volume size</label>
8
+ <input type="number" id="size" formControlName="size" />
9
+ </spk-form-field>
10
+
11
+ <spk-select>
12
+ <input type="text" placeholder="Select an option..." formControlName="unit" />
13
+
14
+ <ng-container options>
15
+ @for (option of sizeOptions(); track $index) {
16
+ <option [value]="option">{{ option }}</option>
17
+ }
18
+ </ng-container>
19
+ </spk-select>
20
+ </div>
21
+
22
+ <p>
23
+ The backups will be split up into multiple files called volumes. Here you can set the maximum size of the
24
+ individual volume files.
25
+ <a href="#" class="spk-primary">Read more</a>
26
+ </p>
27
+ </div>
28
+
29
+ <spk-select>
30
+ <label for="size">Backup retention</label>
31
+ <input type="text" placeholder="Local folder or drive" formControlName="backupRetention" />
32
+
33
+ <ng-container options>
34
+ @for (option of rentationOptions(); track $index) {
35
+ <option [value]="option">{{ option }}</option>
36
+ }
37
+ </ng-container>
38
+ </spk-select>
39
+
40
+ <app-toggle-card [disallowToggle]="true">
41
+ <ng-container title>
42
+ Advanced options
43
+
44
+ <spk-menu>
45
+ <button spk-button type="button" class="outlined small">
46
+ Add advanced option
47
+ <spk-icon>plus</spk-icon>
48
+ </button>
49
+
50
+ <ng-container menu>
51
+ @for (option of nonSelectedAdvancedOptions(); track $index) {
52
+ <button spk-menu-item type="button" (click)="addNewOption(option)">{{ option.Name }}</button>
53
+ }
54
+ </ng-container>
55
+ </spk-menu>
56
+ </ng-container>
57
+
58
+ <ng-container formArrayName="advancedOptions">
59
+ @for (optionCtrl of optionsForm.controls.advancedOptions.controls; track $index) {
60
+ <div class="advanced-option" [formGroup]="optionCtrl">
61
+ <spk-form-field>
62
+ @if ($index === 0) {
63
+ <label for="name">Name</label>
64
+ }
65
+ <input type="text" id="name" formControlName="name" />
66
+ </spk-form-field>
67
+
68
+ <spk-form-field>
69
+ @if ($index === 0) {
70
+ <label for="value">Value</label>
71
+ }
72
+ <input type="text" id="value" formControlName="value" />
73
+ </spk-form-field>
74
+
75
+ <button class="icon" spk-button type="button" (click)="optionsForm.controls.advancedOptions.removeAt($index)">
76
+ <spk-icon>x-circle</spk-icon>
77
+ </button>
78
+ </div>
79
+ }
80
+ </ng-container>
81
+ </app-toggle-card>
82
+
83
+ <div class="form-actions">
84
+ <button spk-button type="button" (click)="goBack()">
85
+ <spk-icon>arrow-left</spk-icon>
86
+ Go back
87
+ </button>
88
+
89
+ <button spk-button class="raised primary" type="submit">
90
+ <spk-icon>arrow-right</spk-icon>
91
+ Submit
92
+ </button>
93
+ </div>
94
+ <!-- <pre>{{ optionsForm.value | json }}</pre> -->
95
+ </form>
@@ -0,0 +1,42 @@
1
+ @use '@sparkle-ui/core/styles/helpers' as *;
2
+
3
+ :host {
4
+ display: block;
5
+ }
6
+
7
+ form {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: p2r(24);
11
+ }
12
+
13
+ .remote-volume-wrap {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: p2r(4);
17
+ }
18
+
19
+ .row-inputs {
20
+ display: flex;
21
+ align-items: flex-end;
22
+ gap: p2r(8);
23
+
24
+ > * {
25
+ flex: 1 0;
26
+ }
27
+
28
+ p {
29
+ }
30
+ }
31
+
32
+ .advanced-option {
33
+ display: grid;
34
+ gap: p2r(8);
35
+ grid-template-columns: 1fr 1fr 40px;
36
+ align-items: flex-end;
37
+ }
38
+
39
+ .form-actions {
40
+ display: flex;
41
+ justify-content: space-between;
42
+ }