diff options
Diffstat (limited to 'client/src/app/helpers')
-rw-r--r-- | client/src/app/helpers/utils.ts | 226 | ||||
-rw-r--r-- | client/src/app/helpers/utils/channel.ts | 34 | ||||
-rw-r--r-- | client/src/app/helpers/utils/date.ts | 25 | ||||
-rw-r--r-- | client/src/app/helpers/utils/html.ts | 18 | ||||
-rw-r--r-- | client/src/app/helpers/utils/index.ts | 7 | ||||
-rw-r--r-- | client/src/app/helpers/utils/object.ts | 47 | ||||
-rw-r--r-- | client/src/app/helpers/utils/ui.ts | 33 | ||||
-rw-r--r-- | client/src/app/helpers/utils/upload.ts | 37 | ||||
-rw-r--r-- | client/src/app/helpers/utils/url.ts | 71 |
9 files changed, 272 insertions, 226 deletions
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts deleted file mode 100644 index 8636f3a55..000000000 --- a/client/src/app/helpers/utils.ts +++ /dev/null | |||
@@ -1,226 +0,0 @@ | |||
1 | import { first, map } from 'rxjs/operators' | ||
2 | import { SelectChannelItem } from 'src/types/select-options-item.model' | ||
3 | import { DatePipe } from '@angular/common' | ||
4 | import { HttpErrorResponse } from '@angular/common/http' | ||
5 | import { Notifier } from '@app/core' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | import { environment } from '../../environments/environment' | ||
8 | import { AuthService } from '../core/auth' | ||
9 | |||
10 | // Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript | ||
11 | function getParameterByName (name: string, url: string) { | ||
12 | if (!url) url = window.location.href | ||
13 | name = name.replace(/[[\]]/g, '\\$&') | ||
14 | |||
15 | const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)') | ||
16 | const results = regex.exec(url) | ||
17 | |||
18 | if (!results) return null | ||
19 | if (!results[2]) return '' | ||
20 | |||
21 | return decodeURIComponent(results[2].replace(/\+/g, ' ')) | ||
22 | } | ||
23 | |||
24 | function listUserChannels (authService: AuthService) { | ||
25 | return authService.userInformationLoaded | ||
26 | .pipe( | ||
27 | first(), | ||
28 | map(() => { | ||
29 | const user = authService.getUser() | ||
30 | if (!user) return undefined | ||
31 | |||
32 | const videoChannels = user.videoChannels | ||
33 | if (Array.isArray(videoChannels) === false) return undefined | ||
34 | |||
35 | return videoChannels | ||
36 | .sort((a, b) => { | ||
37 | if (a.updatedAt < b.updatedAt) return 1 | ||
38 | if (a.updatedAt > b.updatedAt) return -1 | ||
39 | return 0 | ||
40 | }) | ||
41 | .map(c => ({ | ||
42 | id: c.id, | ||
43 | label: c.displayName, | ||
44 | support: c.support, | ||
45 | avatarPath: c.avatar?.path | ||
46 | }) as SelectChannelItem) | ||
47 | }) | ||
48 | ) | ||
49 | } | ||
50 | |||
51 | function getAbsoluteAPIUrl () { | ||
52 | let absoluteAPIUrl = environment.hmr === true | ||
53 | ? 'http://localhost:9000' | ||
54 | : environment.apiUrl | ||
55 | |||
56 | if (!absoluteAPIUrl) { | ||
57 | // The API is on the same domain | ||
58 | absoluteAPIUrl = window.location.origin | ||
59 | } | ||
60 | |||
61 | return absoluteAPIUrl | ||
62 | } | ||
63 | |||
64 | function getAbsoluteEmbedUrl () { | ||
65 | let absoluteEmbedUrl = environment.originServerUrl | ||
66 | if (!absoluteEmbedUrl) { | ||
67 | // The Embed is on the same domain | ||
68 | absoluteEmbedUrl = window.location.origin | ||
69 | } | ||
70 | |||
71 | return absoluteEmbedUrl | ||
72 | } | ||
73 | |||
74 | const datePipe = new DatePipe('en') | ||
75 | function dateToHuman (date: string) { | ||
76 | return datePipe.transform(date, 'medium') | ||
77 | } | ||
78 | |||
79 | function durationToString (duration: number) { | ||
80 | const hours = Math.floor(duration / 3600) | ||
81 | const minutes = Math.floor((duration % 3600) / 60) | ||
82 | const seconds = duration % 60 | ||
83 | |||
84 | const minutesPadding = minutes >= 10 ? '' : '0' | ||
85 | const secondsPadding = seconds >= 10 ? '' : '0' | ||
86 | const displayedHours = hours > 0 ? hours.toString() + ':' : '' | ||
87 | |||
88 | return ( | ||
89 | displayedHours + minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString() | ||
90 | ).replace(/^0/, '') | ||
91 | } | ||
92 | |||
93 | function immutableAssign <A, B> (target: A, source: B) { | ||
94 | return Object.assign({}, target, source) | ||
95 | } | ||
96 | |||
97 | // Thanks: https://gist.github.com/ghinda/8442a57f22099bdb2e34 | ||
98 | function objectToFormData (obj: any, form?: FormData, namespace?: string) { | ||
99 | const fd = form || new FormData() | ||
100 | let formKey | ||
101 | |||
102 | for (const key of Object.keys(obj)) { | ||
103 | if (namespace) formKey = `${namespace}[${key}]` | ||
104 | else formKey = key | ||
105 | |||
106 | if (obj[key] === undefined) continue | ||
107 | |||
108 | if (Array.isArray(obj[key]) && obj[key].length === 0) { | ||
109 | fd.append(key, null) | ||
110 | continue | ||
111 | } | ||
112 | |||
113 | if (obj[key] !== null && typeof obj[key] === 'object' && !(obj[key] instanceof File)) { | ||
114 | objectToFormData(obj[key], fd, formKey) | ||
115 | } else { | ||
116 | fd.append(formKey, obj[key]) | ||
117 | } | ||
118 | } | ||
119 | |||
120 | return fd | ||
121 | } | ||
122 | |||
123 | function objectLineFeedToHtml (obj: any, keyToNormalize: string) { | ||
124 | return immutableAssign(obj, { | ||
125 | [keyToNormalize]: lineFeedToHtml(obj[keyToNormalize]) | ||
126 | }) | ||
127 | } | ||
128 | |||
129 | function lineFeedToHtml (text: string) { | ||
130 | if (!text) return text | ||
131 | |||
132 | return text.replace(/\r?\n|\r/g, '<br />') | ||
133 | } | ||
134 | |||
135 | function removeElementFromArray <T> (arr: T[], elem: T) { | ||
136 | const index = arr.indexOf(elem) | ||
137 | if (index !== -1) arr.splice(index, 1) | ||
138 | } | ||
139 | |||
140 | function sortBy (obj: any[], key1: string, key2?: string) { | ||
141 | return obj.sort((a, b) => { | ||
142 | const elem1 = key2 ? a[key1][key2] : a[key1] | ||
143 | const elem2 = key2 ? b[key1][key2] : b[key1] | ||
144 | |||
145 | if (elem1 < elem2) return -1 | ||
146 | if (elem1 === elem2) return 0 | ||
147 | return 1 | ||
148 | }) | ||
149 | } | ||
150 | |||
151 | function scrollToTop (behavior: 'auto' | 'smooth' = 'auto') { | ||
152 | window.scrollTo({ | ||
153 | left: 0, | ||
154 | top: 0, | ||
155 | behavior | ||
156 | }) | ||
157 | } | ||
158 | |||
159 | function isInViewport (el: HTMLElement) { | ||
160 | const bounding = el.getBoundingClientRect() | ||
161 | return ( | ||
162 | bounding.top >= 0 && | ||
163 | bounding.left >= 0 && | ||
164 | bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) && | ||
165 | bounding.right <= (window.innerWidth || document.documentElement.clientWidth) | ||
166 | ) | ||
167 | } | ||
168 | |||
169 | function isXPercentInViewport (el: HTMLElement, percentVisible: number) { | ||
170 | const rect = el.getBoundingClientRect() | ||
171 | const windowHeight = (window.innerHeight || document.documentElement.clientHeight) | ||
172 | |||
173 | return !( | ||
174 | Math.floor(100 - (((rect.top >= 0 ? 0 : rect.top) / +-(rect.height / 1)) * 100)) < percentVisible || | ||
175 | Math.floor(100 - ((rect.bottom - windowHeight) / rect.height) * 100) < percentVisible | ||
176 | ) | ||
177 | } | ||
178 | |||
179 | function genericUploadErrorHandler (parameters: { | ||
180 | err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'> | ||
181 | name: string | ||
182 | notifier: Notifier | ||
183 | sticky?: boolean | ||
184 | }) { | ||
185 | const { err, name, notifier, sticky } = { sticky: false, ...parameters } | ||
186 | const title = $localize`The upload failed` | ||
187 | let message = err.message | ||
188 | |||
189 | if (err instanceof ErrorEvent) { // network error | ||
190 | message = $localize`The connection was interrupted` | ||
191 | notifier.error(message, title, null, sticky) | ||
192 | } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) { | ||
193 | message = $localize`The server encountered an error` | ||
194 | notifier.error(message, title, null, sticky) | ||
195 | } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { | ||
196 | message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` | ||
197 | notifier.error(message, title, null, sticky) | ||
198 | } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) { | ||
199 | const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G' | ||
200 | message = $localize`Your ${name} file was too large (max. size: ${maxFileSize})` | ||
201 | notifier.error(message, title, null, sticky) | ||
202 | } else { | ||
203 | notifier.error(err.message, title) | ||
204 | } | ||
205 | |||
206 | return message | ||
207 | } | ||
208 | |||
209 | export { | ||
210 | sortBy, | ||
211 | durationToString, | ||
212 | lineFeedToHtml, | ||
213 | getParameterByName, | ||
214 | getAbsoluteAPIUrl, | ||
215 | dateToHuman, | ||
216 | immutableAssign, | ||
217 | objectToFormData, | ||
218 | getAbsoluteEmbedUrl, | ||
219 | objectLineFeedToHtml, | ||
220 | removeElementFromArray, | ||
221 | scrollToTop, | ||
222 | isInViewport, | ||
223 | isXPercentInViewport, | ||
224 | listUserChannels, | ||
225 | genericUploadErrorHandler | ||
226 | } | ||
diff --git a/client/src/app/helpers/utils/channel.ts b/client/src/app/helpers/utils/channel.ts new file mode 100644 index 000000000..93863a8af --- /dev/null +++ b/client/src/app/helpers/utils/channel.ts | |||
@@ -0,0 +1,34 @@ | |||
1 | import { first, map } from 'rxjs/operators' | ||
2 | import { SelectChannelItem } from 'src/types/select-options-item.model' | ||
3 | import { AuthService } from '../../core/auth' | ||
4 | |||
5 | function listUserChannels (authService: AuthService) { | ||
6 | return authService.userInformationLoaded | ||
7 | .pipe( | ||
8 | first(), | ||
9 | map(() => { | ||
10 | const user = authService.getUser() | ||
11 | if (!user) return undefined | ||
12 | |||
13 | const videoChannels = user.videoChannels | ||
14 | if (Array.isArray(videoChannels) === false) return undefined | ||
15 | |||
16 | return videoChannels | ||
17 | .sort((a, b) => { | ||
18 | if (a.updatedAt < b.updatedAt) return 1 | ||
19 | if (a.updatedAt > b.updatedAt) return -1 | ||
20 | return 0 | ||
21 | }) | ||
22 | .map(c => ({ | ||
23 | id: c.id, | ||
24 | label: c.displayName, | ||
25 | support: c.support, | ||
26 | avatarPath: c.avatar?.path | ||
27 | }) as SelectChannelItem) | ||
28 | }) | ||
29 | ) | ||
30 | } | ||
31 | |||
32 | export { | ||
33 | listUserChannels | ||
34 | } | ||
diff --git a/client/src/app/helpers/utils/date.ts b/client/src/app/helpers/utils/date.ts new file mode 100644 index 000000000..012b959ea --- /dev/null +++ b/client/src/app/helpers/utils/date.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { DatePipe } from '@angular/common' | ||
2 | |||
3 | const datePipe = new DatePipe('en') | ||
4 | function dateToHuman (date: string) { | ||
5 | return datePipe.transform(date, 'medium') | ||
6 | } | ||
7 | |||
8 | function durationToString (duration: number) { | ||
9 | const hours = Math.floor(duration / 3600) | ||
10 | const minutes = Math.floor((duration % 3600) / 60) | ||
11 | const seconds = duration % 60 | ||
12 | |||
13 | const minutesPadding = minutes >= 10 ? '' : '0' | ||
14 | const secondsPadding = seconds >= 10 ? '' : '0' | ||
15 | const displayedHours = hours > 0 ? hours.toString() + ':' : '' | ||
16 | |||
17 | return ( | ||
18 | displayedHours + minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString() | ||
19 | ).replace(/^0/, '') | ||
20 | } | ||
21 | |||
22 | export { | ||
23 | durationToString, | ||
24 | dateToHuman | ||
25 | } | ||
diff --git a/client/src/app/helpers/utils/html.ts b/client/src/app/helpers/utils/html.ts new file mode 100644 index 000000000..2d520aee9 --- /dev/null +++ b/client/src/app/helpers/utils/html.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import { immutableAssign } from './object' | ||
2 | |||
3 | function objectLineFeedToHtml (obj: any, keyToNormalize: string) { | ||
4 | return immutableAssign(obj, { | ||
5 | [keyToNormalize]: lineFeedToHtml(obj[keyToNormalize]) | ||
6 | }) | ||
7 | } | ||
8 | |||
9 | function lineFeedToHtml (text: string) { | ||
10 | if (!text) return text | ||
11 | |||
12 | return text.replace(/\r?\n|\r/g, '<br />') | ||
13 | } | ||
14 | |||
15 | export { | ||
16 | objectLineFeedToHtml, | ||
17 | lineFeedToHtml | ||
18 | } | ||
diff --git a/client/src/app/helpers/utils/index.ts b/client/src/app/helpers/utils/index.ts new file mode 100644 index 000000000..dc09c92ab --- /dev/null +++ b/client/src/app/helpers/utils/index.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | export * from './channel' | ||
2 | export * from './date' | ||
3 | export * from './html' | ||
4 | export * from './object' | ||
5 | export * from './ui' | ||
6 | export * from './upload' | ||
7 | export * from './url' | ||
diff --git a/client/src/app/helpers/utils/object.ts b/client/src/app/helpers/utils/object.ts new file mode 100644 index 000000000..1ca4a23ac --- /dev/null +++ b/client/src/app/helpers/utils/object.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | function immutableAssign <A, B> (target: A, source: B) { | ||
2 | return Object.assign({}, target, source) | ||
3 | } | ||
4 | |||
5 | function removeElementFromArray <T> (arr: T[], elem: T) { | ||
6 | const index = arr.indexOf(elem) | ||
7 | if (index !== -1) arr.splice(index, 1) | ||
8 | } | ||
9 | |||
10 | function sortBy (obj: any[], key1: string, key2?: string) { | ||
11 | return obj.sort((a, b) => { | ||
12 | const elem1 = key2 ? a[key1][key2] : a[key1] | ||
13 | const elem2 = key2 ? b[key1][key2] : b[key1] | ||
14 | |||
15 | if (elem1 < elem2) return -1 | ||
16 | if (elem1 === elem2) return 0 | ||
17 | return 1 | ||
18 | }) | ||
19 | } | ||
20 | |||
21 | function intoArray (value: any) { | ||
22 | if (!value) return undefined | ||
23 | if (Array.isArray(value)) return value | ||
24 | |||
25 | if (typeof value === 'string') return value.split(',') | ||
26 | |||
27 | return [ value ] | ||
28 | } | ||
29 | |||
30 | function toBoolean (value: any) { | ||
31 | if (!value) return undefined | ||
32 | |||
33 | if (typeof value === 'boolean') return value | ||
34 | |||
35 | if (value === 'true') return true | ||
36 | if (value === 'false') return false | ||
37 | |||
38 | return undefined | ||
39 | } | ||
40 | |||
41 | export { | ||
42 | sortBy, | ||
43 | immutableAssign, | ||
44 | removeElementFromArray, | ||
45 | intoArray, | ||
46 | toBoolean | ||
47 | } | ||
diff --git a/client/src/app/helpers/utils/ui.ts b/client/src/app/helpers/utils/ui.ts new file mode 100644 index 000000000..ac8298926 --- /dev/null +++ b/client/src/app/helpers/utils/ui.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | function scrollToTop (behavior: 'auto' | 'smooth' = 'auto') { | ||
2 | window.scrollTo({ | ||
3 | left: 0, | ||
4 | top: 0, | ||
5 | behavior | ||
6 | }) | ||
7 | } | ||
8 | |||
9 | function isInViewport (el: HTMLElement) { | ||
10 | const bounding = el.getBoundingClientRect() | ||
11 | return ( | ||
12 | bounding.top >= 0 && | ||
13 | bounding.left >= 0 && | ||
14 | bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) && | ||
15 | bounding.right <= (window.innerWidth || document.documentElement.clientWidth) | ||
16 | ) | ||
17 | } | ||
18 | |||
19 | function isXPercentInViewport (el: HTMLElement, percentVisible: number) { | ||
20 | const rect = el.getBoundingClientRect() | ||
21 | const windowHeight = (window.innerHeight || document.documentElement.clientHeight) | ||
22 | |||
23 | return !( | ||
24 | Math.floor(100 - (((rect.top >= 0 ? 0 : rect.top) / +-(rect.height / 1)) * 100)) < percentVisible || | ||
25 | Math.floor(100 - ((rect.bottom - windowHeight) / rect.height) * 100) < percentVisible | ||
26 | ) | ||
27 | } | ||
28 | |||
29 | export { | ||
30 | scrollToTop, | ||
31 | isInViewport, | ||
32 | isXPercentInViewport | ||
33 | } | ||
diff --git a/client/src/app/helpers/utils/upload.ts b/client/src/app/helpers/utils/upload.ts new file mode 100644 index 000000000..a3fce7fee --- /dev/null +++ b/client/src/app/helpers/utils/upload.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import { HttpErrorResponse } from '@angular/common/http' | ||
2 | import { Notifier } from '@app/core' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | |||
5 | function genericUploadErrorHandler (parameters: { | ||
6 | err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'> | ||
7 | name: string | ||
8 | notifier: Notifier | ||
9 | sticky?: boolean | ||
10 | }) { | ||
11 | const { err, name, notifier, sticky } = { sticky: false, ...parameters } | ||
12 | const title = $localize`The upload failed` | ||
13 | let message = err.message | ||
14 | |||
15 | if (err instanceof ErrorEvent) { // network error | ||
16 | message = $localize`The connection was interrupted` | ||
17 | notifier.error(message, title, null, sticky) | ||
18 | } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) { | ||
19 | message = $localize`The server encountered an error` | ||
20 | notifier.error(message, title, null, sticky) | ||
21 | } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { | ||
22 | message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` | ||
23 | notifier.error(message, title, null, sticky) | ||
24 | } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) { | ||
25 | const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G' | ||
26 | message = $localize`Your ${name} file was too large (max. size: ${maxFileSize})` | ||
27 | notifier.error(message, title, null, sticky) | ||
28 | } else { | ||
29 | notifier.error(err.message, title) | ||
30 | } | ||
31 | |||
32 | return message | ||
33 | } | ||
34 | |||
35 | export { | ||
36 | genericUploadErrorHandler | ||
37 | } | ||
diff --git a/client/src/app/helpers/utils/url.ts b/client/src/app/helpers/utils/url.ts new file mode 100644 index 000000000..82d9cc11b --- /dev/null +++ b/client/src/app/helpers/utils/url.ts | |||
@@ -0,0 +1,71 @@ | |||
1 | import { environment } from '../../../environments/environment' | ||
2 | |||
3 | // Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript | ||
4 | function getParameterByName (name: string, url: string) { | ||
5 | if (!url) url = window.location.href | ||
6 | name = name.replace(/[[\]]/g, '\\$&') | ||
7 | |||
8 | const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)') | ||
9 | const results = regex.exec(url) | ||
10 | |||
11 | if (!results) return null | ||
12 | if (!results[2]) return '' | ||
13 | |||
14 | return decodeURIComponent(results[2].replace(/\+/g, ' ')) | ||
15 | } | ||
16 | |||
17 | function getAbsoluteAPIUrl () { | ||
18 | let absoluteAPIUrl = environment.hmr === true | ||
19 | ? 'http://localhost:9000' | ||
20 | : environment.apiUrl | ||
21 | |||
22 | if (!absoluteAPIUrl) { | ||
23 | // The API is on the same domain | ||
24 | absoluteAPIUrl = window.location.origin | ||
25 | } | ||
26 | |||
27 | return absoluteAPIUrl | ||
28 | } | ||
29 | |||
30 | function getAbsoluteEmbedUrl () { | ||
31 | let absoluteEmbedUrl = environment.originServerUrl | ||
32 | if (!absoluteEmbedUrl) { | ||
33 | // The Embed is on the same domain | ||
34 | absoluteEmbedUrl = window.location.origin | ||
35 | } | ||
36 | |||
37 | return absoluteEmbedUrl | ||
38 | } | ||
39 | |||
40 | // Thanks: https://gist.github.com/ghinda/8442a57f22099bdb2e34 | ||
41 | function objectToFormData (obj: any, form?: FormData, namespace?: string) { | ||
42 | const fd = form || new FormData() | ||
43 | let formKey | ||
44 | |||
45 | for (const key of Object.keys(obj)) { | ||
46 | if (namespace) formKey = `${namespace}[${key}]` | ||
47 | else formKey = key | ||
48 | |||
49 | if (obj[key] === undefined) continue | ||
50 | |||
51 | if (Array.isArray(obj[key]) && obj[key].length === 0) { | ||
52 | fd.append(key, null) | ||
53 | continue | ||
54 | } | ||
55 | |||
56 | if (obj[key] !== null && typeof obj[key] === 'object' && !(obj[key] instanceof File)) { | ||
57 | objectToFormData(obj[key], fd, formKey) | ||
58 | } else { | ||
59 | fd.append(formKey, obj[key]) | ||
60 | } | ||
61 | } | ||
62 | |||
63 | return fd | ||
64 | } | ||
65 | |||
66 | export { | ||
67 | getParameterByName, | ||
68 | objectToFormData, | ||
69 | getAbsoluteAPIUrl, | ||
70 | getAbsoluteEmbedUrl | ||
71 | } | ||