diff options
author | Chocobozzz <me@florianbigard.com> | 2023-08-28 10:55:04 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-28 16:17:31 +0200 |
commit | 77b70702d2193d78bf6fbd07f0fc7335e34957f8 (patch) | |
tree | 1a0aed540054286c9a8b10c4890cc0f718e00458 /client/src/app/shared | |
parent | 7113f32a87bd6b2868154fed20bde1a1633c190e (diff) | |
download | PeerTube-77b70702d2193d78bf6fbd07f0fc7335e34957f8.tar.gz PeerTube-77b70702d2193d78bf6fbd07f0fc7335e34957f8.tar.zst PeerTube-77b70702d2193d78bf6fbd07f0fc7335e34957f8.zip |
Add video chapters support
Diffstat (limited to 'client/src/app/shared')
14 files changed, 180 insertions, 25 deletions
diff --git a/client/src/app/shared/form-validators/video-chapter-validators.ts b/client/src/app/shared/form-validators/video-chapter-validators.ts new file mode 100644 index 000000000..cbbd9291e --- /dev/null +++ b/client/src/app/shared/form-validators/video-chapter-validators.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | import { AbstractControl, ValidationErrors, ValidatorFn, Validators } from '@angular/forms' | ||
2 | import { BuildFormValidator } from './form-validator.model' | ||
3 | |||
4 | export const VIDEO_CHAPTER_TITLE_VALIDATOR: BuildFormValidator = { | ||
5 | VALIDATORS: [ Validators.minLength(2), Validators.maxLength(100) ], // Required is set dynamically | ||
6 | MESSAGES: { | ||
7 | required: $localize`A chapter title is required.`, | ||
8 | minlength: $localize`A chapter title should be more than 2 characters long.`, | ||
9 | maxlength: $localize`A chapter title should be less than 100 characters long.` | ||
10 | } | ||
11 | } | ||
12 | |||
13 | export const VIDEO_CHAPTERS_ARRAY_VALIDATOR: BuildFormValidator = { | ||
14 | VALIDATORS: [ uniqueTimecodeValidator() ], | ||
15 | MESSAGES: {} | ||
16 | } | ||
17 | |||
18 | function uniqueTimecodeValidator (): ValidatorFn { | ||
19 | return (control: AbstractControl): ValidationErrors => { | ||
20 | const array = control.value as { timecode: number, title: string }[] | ||
21 | |||
22 | for (const chapter of array) { | ||
23 | if (!chapter.title) continue | ||
24 | |||
25 | if (array.filter(c => c.title && c.timecode === chapter.timecode).length > 1) { | ||
26 | return { uniqueTimecode: $localize`Multiple chapters have the same timecode ${chapter.timecode}` } | ||
27 | } | ||
28 | } | ||
29 | |||
30 | return null | ||
31 | } | ||
32 | } | ||
diff --git a/client/src/app/shared/form-validators/video-validators.ts b/client/src/app/shared/form-validators/video-validators.ts index 090a76e43..a434c777f 100644 --- a/client/src/app/shared/form-validators/video-validators.ts +++ b/client/src/app/shared/form-validators/video-validators.ts | |||
@@ -70,14 +70,6 @@ export const VIDEO_DESCRIPTION_VALIDATOR: BuildFormValidator = { | |||
70 | } | 70 | } |
71 | } | 71 | } |
72 | 72 | ||
73 | export const VIDEO_TAG_VALIDATOR: BuildFormValidator = { | ||
74 | VALIDATORS: [ Validators.minLength(2), Validators.maxLength(30) ], | ||
75 | MESSAGES: { | ||
76 | minlength: $localize`A tag should be more than 2 characters long.`, | ||
77 | maxlength: $localize`A tag should be less than 30 characters long.` | ||
78 | } | ||
79 | } | ||
80 | |||
81 | export const VIDEO_TAGS_ARRAY_VALIDATOR: BuildFormValidator = { | 73 | export const VIDEO_TAGS_ARRAY_VALIDATOR: BuildFormValidator = { |
82 | VALIDATORS: [ Validators.maxLength(5), arrayTagLengthValidator() ], | 74 | VALIDATORS: [ Validators.maxLength(5), arrayTagLengthValidator() ], |
83 | MESSAGES: { | 75 | MESSAGES: { |
diff --git a/client/src/app/shared/shared-forms/form-reactive.service.ts b/client/src/app/shared/shared-forms/form-reactive.service.ts index f1b7e0ef2..b960c310e 100644 --- a/client/src/app/shared/shared-forms/form-reactive.service.ts +++ b/client/src/app/shared/shared-forms/form-reactive.service.ts | |||
@@ -4,9 +4,9 @@ import { wait } from '@root-helpers/utils' | |||
4 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' | 4 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' |
5 | import { FormValidatorService } from './form-validator.service' | 5 | import { FormValidatorService } from './form-validator.service' |
6 | 6 | ||
7 | export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors } | 7 | export type FormReactiveErrors = { [ id: string | number ]: string | FormReactiveErrors | FormReactiveErrors[] } |
8 | export type FormReactiveValidationMessages = { | 8 | export type FormReactiveValidationMessages = { |
9 | [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages | 9 | [ id: string | number ]: { [ name: string ]: string } | FormReactiveValidationMessages | FormReactiveValidationMessages[] |
10 | } | 10 | } |
11 | 11 | ||
12 | @Injectable() | 12 | @Injectable() |
@@ -86,7 +86,7 @@ export class FormReactiveService { | |||
86 | 86 | ||
87 | if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue | 87 | if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue |
88 | 88 | ||
89 | const staticMessages = validationMessages[field] | 89 | const staticMessages = validationMessages[field] as FormReactiveValidationMessages |
90 | for (const key of Object.keys(control.errors)) { | 90 | for (const key of Object.keys(control.errors)) { |
91 | const formErrorValue = control.errors[key] | 91 | const formErrorValue = control.errors[key] |
92 | 92 | ||
diff --git a/client/src/app/shared/shared-forms/form-validator.service.ts b/client/src/app/shared/shared-forms/form-validator.service.ts index e7dedf52a..d810285bb 100644 --- a/client/src/app/shared/shared-forms/form-validator.service.ts +++ b/client/src/app/shared/shared-forms/form-validator.service.ts | |||
@@ -45,20 +45,20 @@ export class FormValidatorService { | |||
45 | form: FormGroup, | 45 | form: FormGroup, |
46 | formErrors: FormReactiveErrors, | 46 | formErrors: FormReactiveErrors, |
47 | validationMessages: FormReactiveValidationMessages, | 47 | validationMessages: FormReactiveValidationMessages, |
48 | obj: BuildFormArgument, | 48 | formToBuild: BuildFormArgument, |
49 | defaultValues: BuildFormDefaultValues = {} | 49 | defaultValues: BuildFormDefaultValues = {} |
50 | ) { | 50 | ) { |
51 | for (const name of objectKeysTyped(obj)) { | 51 | for (const name of objectKeysTyped(formToBuild)) { |
52 | formErrors[name] = '' | 52 | formErrors[name] = '' |
53 | 53 | ||
54 | const field = obj[name] | 54 | const field = formToBuild[name] |
55 | if (this.isRecursiveField(field)) { | 55 | if (this.isRecursiveField(field)) { |
56 | this.updateFormGroup( | 56 | this.updateFormGroup( |
57 | // FIXME: typings | 57 | // FIXME: typings |
58 | (form as any)[name], | 58 | (form as any)[name], |
59 | formErrors[name] as FormReactiveErrors, | 59 | formErrors[name] as FormReactiveErrors, |
60 | validationMessages[name] as FormReactiveValidationMessages, | 60 | validationMessages[name] as FormReactiveValidationMessages, |
61 | obj[name] as BuildFormArgument, | 61 | formToBuild[name] as BuildFormArgument, |
62 | defaultValues[name] as BuildFormDefaultValues | 62 | defaultValues[name] as BuildFormDefaultValues |
63 | ) | 63 | ) |
64 | continue | 64 | continue |
@@ -66,7 +66,7 @@ export class FormValidatorService { | |||
66 | 66 | ||
67 | if (field?.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string } | 67 | if (field?.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string } |
68 | 68 | ||
69 | const defaultValue = defaultValues[name] || '' | 69 | const defaultValue = defaultValues[name] ?? '' |
70 | 70 | ||
71 | form.addControl( | 71 | form.addControl( |
72 | name + '', | 72 | name + '', |
@@ -75,6 +75,55 @@ export class FormValidatorService { | |||
75 | } | 75 | } |
76 | } | 76 | } |
77 | 77 | ||
78 | addControlInFormArray (options: { | ||
79 | formErrors: FormReactiveErrors | ||
80 | validationMessages: FormReactiveValidationMessages | ||
81 | formArray: FormArray | ||
82 | controlName: string | ||
83 | formToBuild: BuildFormArgument | ||
84 | defaultValues?: BuildFormDefaultValues | ||
85 | }) { | ||
86 | const { formArray, formErrors, validationMessages, controlName, formToBuild, defaultValues = {} } = options | ||
87 | |||
88 | const formGroup = new FormGroup({}) | ||
89 | if (!formErrors[controlName]) formErrors[controlName] = [] as FormReactiveErrors[] | ||
90 | if (!validationMessages[controlName]) validationMessages[controlName] = [] | ||
91 | |||
92 | const formArrayErrors = formErrors[controlName] as FormReactiveErrors[] | ||
93 | const formArrayValidationMessages = validationMessages[controlName] as FormReactiveValidationMessages[] | ||
94 | |||
95 | const totalControls = formArray.controls.length | ||
96 | formArrayErrors.push({}) | ||
97 | formArrayValidationMessages.push({}) | ||
98 | |||
99 | this.updateFormGroup( | ||
100 | formGroup, | ||
101 | formArrayErrors[totalControls], | ||
102 | formArrayValidationMessages[totalControls], | ||
103 | formToBuild, | ||
104 | defaultValues | ||
105 | ) | ||
106 | |||
107 | formArray.push(formGroup) | ||
108 | } | ||
109 | |||
110 | removeControlFromFormArray (options: { | ||
111 | formErrors: FormReactiveErrors | ||
112 | validationMessages: FormReactiveValidationMessages | ||
113 | index: number | ||
114 | formArray: FormArray | ||
115 | controlName: string | ||
116 | }) { | ||
117 | const { formArray, formErrors, validationMessages, index, controlName } = options | ||
118 | |||
119 | const formArrayErrors = formErrors[controlName] as FormReactiveErrors[] | ||
120 | const formArrayValidationMessages = validationMessages[controlName] as FormReactiveValidationMessages[] | ||
121 | |||
122 | formArrayErrors.splice(index, 1) | ||
123 | formArrayValidationMessages.splice(index, 1) | ||
124 | formArray.removeAt(index) | ||
125 | } | ||
126 | |||
78 | updateTreeValidity (group: FormGroup | FormArray): void { | 127 | updateTreeValidity (group: FormGroup | FormArray): void { |
79 | for (const key of Object.keys(group.controls)) { | 128 | for (const key of Object.keys(group.controls)) { |
80 | // FIXME: typings | 129 | // FIXME: typings |
diff --git a/client/src/app/shared/shared-forms/input-text.component.ts b/client/src/app/shared/shared-forms/input-text.component.ts index be03f25b9..2f3c8f603 100644 --- a/client/src/app/shared/shared-forms/input-text.component.ts +++ b/client/src/app/shared/shared-forms/input-text.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Notifier } from '@app/core' | 3 | import { FormReactiveErrors } from './form-reactive.service' |
4 | 4 | ||
5 | @Component({ | 5 | @Component({ |
6 | selector: 'my-input-text', | 6 | selector: 'my-input-text', |
@@ -26,9 +26,7 @@ export class InputTextComponent implements ControlValueAccessor { | |||
26 | @Input() withCopy = false | 26 | @Input() withCopy = false |
27 | @Input() readonly = false | 27 | @Input() readonly = false |
28 | @Input() show = false | 28 | @Input() show = false |
29 | @Input() formError: string | 29 | @Input() formError: string | FormReactiveErrors | FormReactiveErrors[] |
30 | |||
31 | constructor (private notifier: Notifier) { } | ||
32 | 30 | ||
33 | get inputType () { | 31 | get inputType () { |
34 | return this.show | 32 | return this.show |
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.html b/client/src/app/shared/shared-forms/markdown-textarea.component.html index ac2dfd17c..7f8bd2f62 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.html +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.html | |||
@@ -25,7 +25,7 @@ | |||
25 | </ng-template> | 25 | </ng-template> |
26 | </ng-container> | 26 | </ng-container> |
27 | 27 | ||
28 | <button (click)="onMaximizeClick()" class="maximize-button border-0 m-3" [disabled]="disabled"> | 28 | <button type="button" (click)="onMaximizeClick()" class="maximize-button border-0 m-3" [disabled]="disabled"> |
29 | <my-global-icon *ngIf="!isMaximized" [ngbTooltip]="maximizeInText" iconName="fullscreen"></my-global-icon> | 29 | <my-global-icon *ngIf="!isMaximized" [ngbTooltip]="maximizeInText" iconName="fullscreen"></my-global-icon> |
30 | 30 | ||
31 | <my-global-icon *ngIf="isMaximized" [ngbTooltip]="maximizeOutText" iconName="exit-fullscreen"></my-global-icon> | 31 | <my-global-icon *ngIf="isMaximized" [ngbTooltip]="maximizeOutText" iconName="exit-fullscreen"></my-global-icon> |
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts index 169be39d1..77e6cbd8c 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts | |||
@@ -6,6 +6,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | |||
6 | import { SafeHtml } from '@angular/platform-browser' | 6 | import { SafeHtml } from '@angular/platform-browser' |
7 | import { MarkdownService, ScreenService } from '@app/core' | 7 | import { MarkdownService, ScreenService } from '@app/core' |
8 | import { Video } from '@peertube/peertube-models' | 8 | import { Video } from '@peertube/peertube-models' |
9 | import { FormReactiveErrors } from './form-reactive.service' | ||
9 | 10 | ||
10 | @Component({ | 11 | @Component({ |
11 | selector: 'my-markdown-textarea', | 12 | selector: 'my-markdown-textarea', |
@@ -23,7 +24,7 @@ import { Video } from '@peertube/peertube-models' | |||
23 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | 24 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { |
24 | @Input() content = '' | 25 | @Input() content = '' |
25 | 26 | ||
26 | @Input() formError: string | 27 | @Input() formError: string | FormReactiveErrors | FormReactiveErrors[] |
27 | 28 | ||
28 | @Input() truncateTo3Lines: boolean | 29 | @Input() truncateTo3Lines: boolean |
29 | 30 | ||
diff --git a/client/src/app/shared/shared-forms/timestamp-input.component.scss b/client/src/app/shared/shared-forms/timestamp-input.component.scss index e69a06947..df19240b4 100644 --- a/client/src/app/shared/shared-forms/timestamp-input.component.scss +++ b/client/src/app/shared/shared-forms/timestamp-input.component.scss | |||
@@ -4,6 +4,7 @@ | |||
4 | p-inputmask { | 4 | p-inputmask { |
5 | ::ng-deep input { | 5 | ::ng-deep input { |
6 | width: 80px; | 6 | width: 80px; |
7 | text-align: center; | ||
7 | 8 | ||
8 | &:focus-within, | 9 | &:focus-within, |
9 | &:focus { | 10 | &:focus { |
diff --git a/client/src/app/shared/shared-main/buttons/button.component.html b/client/src/app/shared/shared-main/buttons/button.component.html index d87e35876..9270c0925 100644 --- a/client/src/app/shared/shared-main/buttons/button.component.html +++ b/client/src/app/shared/shared-main/buttons/button.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <button *ngIf="!ptRouterLink" class="action-button" [ngClass]="classes" [ngbTooltip]="title"> | 1 | <button *ngIf="!ptRouterLink" type="button" class="action-button" [ngClass]="classes" [ngbTooltip]="title"> |
2 | <ng-container *ngTemplateOutlet="content"></ng-container> | 2 | <ng-container *ngTemplateOutlet="content"></ng-container> |
3 | </button> | 3 | </button> |
4 | 4 | ||
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 243394bda..30c6cabf5 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -49,6 +49,7 @@ import { UserHistoryService, UserNotificationsComponent, UserNotificationService | |||
49 | import { | 49 | import { |
50 | EmbedComponent, | 50 | EmbedComponent, |
51 | RedundancyService, | 51 | RedundancyService, |
52 | VideoChapterService, | ||
52 | VideoFileTokenService, | 53 | VideoFileTokenService, |
53 | VideoImportService, | 54 | VideoImportService, |
54 | VideoOwnershipService, | 55 | VideoOwnershipService, |
@@ -215,6 +216,8 @@ import { VideoChannelService } from './video-channel' | |||
215 | 216 | ||
216 | VideoPasswordService, | 217 | VideoPasswordService, |
217 | 218 | ||
219 | VideoChapterService, | ||
220 | |||
218 | CustomPageService, | 221 | CustomPageService, |
219 | 222 | ||
220 | ActorRedirectGuard | 223 | ActorRedirectGuard |
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts index 59c0969a9..5e4a27d4e 100644 --- a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts +++ b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts | |||
@@ -3,9 +3,9 @@ import { catchError, map, switchMap } from 'rxjs/operators' | |||
3 | import { HttpClient } from '@angular/common/http' | 3 | import { HttpClient } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { RestExtractor, ServerService } from '@app/core' | 5 | import { RestExtractor, ServerService } from '@app/core' |
6 | import { objectToFormData, sortBy } from '@app/helpers' | 6 | import { objectToFormData } from '@app/helpers' |
7 | import { VideoPasswordService, VideoService } from '@app/shared/shared-main/video' | 7 | import { VideoPasswordService, VideoService } from '@app/shared/shared-main/video' |
8 | import { peertubeTranslate } from '@peertube/peertube-core-utils' | 8 | import { peertubeTranslate, sortBy } from '@peertube/peertube-core-utils' |
9 | import { ResultList, VideoCaption } from '@peertube/peertube-models' | 9 | import { ResultList, VideoCaption } from '@peertube/peertube-models' |
10 | import { environment } from '../../../../environments/environment' | 10 | import { environment } from '../../../../environments/environment' |
11 | import { VideoCaptionEdit } from './video-caption-edit.model' | 11 | import { VideoCaptionEdit } from './video-caption-edit.model' |
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts index 07d40b117..7414ded23 100644 --- a/client/src/app/shared/shared-main/video/index.ts +++ b/client/src/app/shared/shared-main/video/index.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | export * from './embed.component' | 1 | export * from './embed.component' |
2 | export * from './redundancy.service' | 2 | export * from './redundancy.service' |
3 | export * from './video-chapter.service' | ||
4 | export * from './video-chapters-edit.model' | ||
3 | export * from './video-details.model' | 5 | export * from './video-details.model' |
4 | export * from './video-edit.model' | 6 | export * from './video-edit.model' |
5 | export * from './video-file-token.service' | 7 | export * from './video-file-token.service' |
diff --git a/client/src/app/shared/shared-main/video/video-chapter.service.ts b/client/src/app/shared/shared-main/video/video-chapter.service.ts new file mode 100644 index 000000000..6d221c9e9 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-chapter.service.ts | |||
@@ -0,0 +1,34 @@ | |||
1 | import { catchError } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor } from '@app/core' | ||
5 | import { VideoChapter, VideoChapterUpdate } from '@peertube/peertube-models' | ||
6 | import { VideoPasswordService } from './video-password.service' | ||
7 | import { VideoService } from './video.service' | ||
8 | import { VideoChaptersEdit } from './video-chapters-edit.model' | ||
9 | import { of } from 'rxjs' | ||
10 | |||
11 | @Injectable() | ||
12 | export class VideoChapterService { | ||
13 | |||
14 | constructor ( | ||
15 | private authHttp: HttpClient, | ||
16 | private restExtractor: RestExtractor | ||
17 | ) {} | ||
18 | |||
19 | getChapters (options: { videoId: string, videoPassword?: string }) { | ||
20 | const headers = VideoPasswordService.buildVideoPasswordHeader(options.videoPassword) | ||
21 | |||
22 | return this.authHttp.get<{ chapters: VideoChapter[] }>(`${VideoService.BASE_VIDEO_URL}/${options.videoId}/chapters`, { headers }) | ||
23 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
24 | } | ||
25 | |||
26 | updateChapters (videoId: string, chaptersEdit: VideoChaptersEdit) { | ||
27 | if (chaptersEdit.shouldUpdateAPI() !== true) return of(true) | ||
28 | |||
29 | const body = { chapters: chaptersEdit.getChaptersForUpdate() } as VideoChapterUpdate | ||
30 | |||
31 | return this.authHttp.put(`${VideoService.BASE_VIDEO_URL}/${videoId}/chapters`, body) | ||
32 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
33 | } | ||
34 | } | ||
diff --git a/client/src/app/shared/shared-main/video/video-chapters-edit.model.ts b/client/src/app/shared/shared-main/video/video-chapters-edit.model.ts new file mode 100644 index 000000000..6d7496ed6 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-chapters-edit.model.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | import { simpleObjectsDeepEqual, sortBy } from '@peertube/peertube-core-utils' | ||
2 | import { VideoChapter } from '@peertube/peertube-models' | ||
3 | |||
4 | export class VideoChaptersEdit { | ||
5 | private chaptersFromAPI: VideoChapter[] = [] | ||
6 | |||
7 | private chapters: VideoChapter[] | ||
8 | |||
9 | loadFromAPI (chapters: VideoChapter[]) { | ||
10 | this.chapters = chapters || [] | ||
11 | |||
12 | this.chaptersFromAPI = chapters | ||
13 | } | ||
14 | |||
15 | patch (values: { [ id: string ]: any }) { | ||
16 | const chapters = values.chapters || [] | ||
17 | |||
18 | this.chapters = chapters.map((c: any) => { | ||
19 | return { | ||
20 | timecode: c.timecode || 0, | ||
21 | title: c.title | ||
22 | } | ||
23 | }) | ||
24 | } | ||
25 | |||
26 | toFormPatch () { | ||
27 | return { chapters: this.chapters } | ||
28 | } | ||
29 | |||
30 | getChaptersForUpdate (): VideoChapter[] { | ||
31 | return this.chapters.filter(c => !!c.title) | ||
32 | } | ||
33 | |||
34 | hasDuplicateValues () { | ||
35 | const timecodes = this.chapters.map(c => c.timecode) | ||
36 | |||
37 | return new Set(timecodes).size !== this.chapters.length | ||
38 | } | ||
39 | |||
40 | shouldUpdateAPI () { | ||
41 | return simpleObjectsDeepEqual(sortBy(this.getChaptersForUpdate(), 'timecode'), this.chaptersFromAPI) !== true | ||
42 | } | ||
43 | } | ||