aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared')
-rw-r--r--client/src/app/shared/shared-forms/form-validators/video-validators.service.ts27
-rw-r--r--client/src/app/shared/shared-forms/select-channel.component.html16
-rw-r--r--client/src/app/shared/shared-forms/select-channel.component.ts51
-rw-r--r--client/src/app/shared/shared-forms/select-options.component.html18
-rw-r--r--client/src/app/shared/shared-forms/select-options.component.ts47
-rw-r--r--client/src/app/shared/shared-forms/select-shared.component.scss20
-rw-r--r--client/src/app/shared/shared-forms/select-tags.component.html13
-rw-r--r--client/src/app/shared/shared-forms/select-tags.component.scss3
-rw-r--r--client/src/app/shared/shared-forms/select-tags.component.ts38
-rw-r--r--client/src/app/shared/shared-forms/shared-form.module.ts13
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts7
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts12
12 files changed, 254 insertions, 11 deletions
diff --git a/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts
index 9b24e4f62..c96e4ef66 100644
--- a/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts
+++ b/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts
@@ -1,5 +1,5 @@
1import { I18n } from '@ngx-translate/i18n-polyfill' 1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Validators } from '@angular/forms' 2import { Validators, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { BuildFormValidator } from './form-validator.service' 4import { BuildFormValidator } from './form-validator.service'
5 5
@@ -13,7 +13,8 @@ export class VideoValidatorsService {
13 readonly VIDEO_IMAGE: BuildFormValidator 13 readonly VIDEO_IMAGE: BuildFormValidator
14 readonly VIDEO_CHANNEL: BuildFormValidator 14 readonly VIDEO_CHANNEL: BuildFormValidator
15 readonly VIDEO_DESCRIPTION: BuildFormValidator 15 readonly VIDEO_DESCRIPTION: BuildFormValidator
16 readonly VIDEO_TAGS: BuildFormValidator 16 readonly VIDEO_TAGS_ARRAY: BuildFormValidator
17 readonly VIDEO_TAG: BuildFormValidator
17 readonly VIDEO_SUPPORT: BuildFormValidator 18 readonly VIDEO_SUPPORT: BuildFormValidator
18 readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator 19 readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator
19 readonly VIDEO_ORIGINALLY_PUBLISHED_AT: BuildFormValidator 20 readonly VIDEO_ORIGINALLY_PUBLISHED_AT: BuildFormValidator
@@ -71,7 +72,7 @@ export class VideoValidatorsService {
71 } 72 }
72 } 73 }
73 74
74 this.VIDEO_TAGS = { 75 this.VIDEO_TAG = {
75 VALIDATORS: [ Validators.minLength(2), Validators.maxLength(30) ], 76 VALIDATORS: [ Validators.minLength(2), Validators.maxLength(30) ],
76 MESSAGES: { 77 MESSAGES: {
77 'minlength': this.i18n('A tag should be more than 2 characters long.'), 78 'minlength': this.i18n('A tag should be more than 2 characters long.'),
@@ -79,6 +80,14 @@ export class VideoValidatorsService {
79 } 80 }
80 } 81 }
81 82
83 this.VIDEO_TAGS_ARRAY = {
84 VALIDATORS: [ Validators.maxLength(5), this.arrayTagLengthValidator() ],
85 MESSAGES: {
86 'maxlength': this.i18n('A maximum of 5 tags can be used on a video.'),
87 'arrayTagLength': this.i18n('A tag should be more than 2, and less than 30 characters long.')
88 }
89 }
90
82 this.VIDEO_SUPPORT = { 91 this.VIDEO_SUPPORT = {
83 VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ], 92 VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ],
84 MESSAGES: { 93 MESSAGES: {
@@ -99,4 +108,16 @@ export class VideoValidatorsService {
99 MESSAGES: {} 108 MESSAGES: {}
100 } 109 }
101 } 110 }
111
112 arrayTagLengthValidator (min = 2, max = 30): ValidatorFn {
113 return (control: AbstractControl): ValidationErrors => {
114 const array = control.value as Array<string>
115
116 if (array.every(e => e.length > min && e.length < max)) {
117 return null
118 }
119
120 return { 'arrayTagLength': true }
121 }
122 }
102} 123}
diff --git a/client/src/app/shared/shared-forms/select-channel.component.html b/client/src/app/shared/shared-forms/select-channel.component.html
new file mode 100644
index 000000000..897d13ee7
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select-channel.component.html
@@ -0,0 +1,16 @@
1<ng-select
2 [(ngModel)]="selectedId"
3 (ngModelChange)="onModelChange()"
4 [bindLabel]="bindLabel"
5 [bindValue]="bindValue"
6 [clearable]="clearable"
7 [searchable]="searchable"
8>
9 <ng-option *ngFor="let channel of channels" [value]="channel.id">
10 <img
11 class="avatar mr-1"
12 [src]="channel.avatarPath"
13 />
14 {{ channel.label }}
15 </ng-option>
16</ng-select>
diff --git a/client/src/app/shared/shared-forms/select-channel.component.ts b/client/src/app/shared/shared-forms/select-channel.component.ts
new file mode 100644
index 000000000..de98c8c0a
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select-channel.component.ts
@@ -0,0 +1,51 @@
1import { Component, Input, forwardRef, ViewChild } from '@angular/core'
2import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
3import { Actor } from '../shared-main'
4
5@Component({
6 selector: 'my-select-channel',
7 styleUrls: [ './select-shared.component.scss' ],
8 templateUrl: './select-channel.component.html',
9 providers: [
10 {
11 provide: NG_VALUE_ACCESSOR,
12 useExisting: forwardRef(() => SelectChannelComponent),
13 multi: true
14 }
15 ]
16})
17export class SelectChannelComponent implements ControlValueAccessor {
18 @Input() items: { id: number, label: string, support: string, avatarPath?: string }[] = []
19
20 selectedId: number
21
22 // ng-select options
23 bindLabel = 'label'
24 bindValue = 'id'
25 clearable = false
26 searchable = false
27
28 get channels () {
29 return this.items.map(c => Object.assign(c, {
30 avatarPath: c.avatarPath ? c.avatarPath : Actor.GET_DEFAULT_AVATAR_URL()
31 }))
32 }
33
34 propagateChange = (_: any) => { /* empty */ }
35
36 writeValue (id: number) {
37 this.selectedId = id
38 }
39
40 registerOnChange (fn: (_: any) => void) {
41 this.propagateChange = fn
42 }
43
44 registerOnTouched () {
45 // Unused
46 }
47
48 onModelChange () {
49 this.propagateChange(this.selectedId)
50 }
51}
diff --git a/client/src/app/shared/shared-forms/select-options.component.html b/client/src/app/shared/shared-forms/select-options.component.html
new file mode 100644
index 000000000..fda0c2c56
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select-options.component.html
@@ -0,0 +1,18 @@
1<ng-select
2 [items]="items"
3 [groupBy]="groupBy"
4 [(ngModel)]="selectedId"
5 (ngModelChange)="onModelChange()"
6 [bindLabel]="bindLabel"
7 [bindValue]="bindValue"
8 [clearable]="clearable"
9 [searchable]="searchable"
10>
11 <ng-template ng-option-tmp let-item="item" let-index="index">
12 {{ item.label }}
13 <ng-container *ngIf="item.description">
14 <br>
15 <span [title]="item.description" class="text-muted">{{ item.description }}</span>
16 </ng-container>
17 </ng-template>
18</ng-select>
diff --git a/client/src/app/shared/shared-forms/select-options.component.ts b/client/src/app/shared/shared-forms/select-options.component.ts
new file mode 100644
index 000000000..09f7df53b
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select-options.component.ts
@@ -0,0 +1,47 @@
1import { Component, Input, forwardRef } from '@angular/core'
2import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
3
4export type SelectOptionsItem = { id: number | string, label: string, description?: string }
5
6@Component({
7 selector: 'my-select-options',
8 styleUrls: [ './select-shared.component.scss' ],
9 templateUrl: './select-options.component.html',
10 providers: [
11 {
12 provide: NG_VALUE_ACCESSOR,
13 useExisting: forwardRef(() => SelectOptionsComponent),
14 multi: true
15 }
16 ]
17})
18export class SelectOptionsComponent implements ControlValueAccessor {
19 @Input() items: SelectOptionsItem[] = []
20 @Input() clearable = false
21 @Input() searchable = false
22 @Input() bindValue = 'id'
23 @Input() groupBy: string
24
25 selectedId: number | string
26
27 // ng-select options
28 bindLabel = 'label'
29
30 propagateChange = (_: any) => { /* empty */ }
31
32 writeValue (id: number | string) {
33 this.selectedId = id
34 }
35
36 registerOnChange (fn: (_: any) => void) {
37 this.propagateChange = fn
38 }
39
40 registerOnTouched () {
41 // Unused
42 }
43
44 onModelChange () {
45 this.propagateChange(this.selectedId)
46 }
47}
diff --git a/client/src/app/shared/shared-forms/select-shared.component.scss b/client/src/app/shared/shared-forms/select-shared.component.scss
new file mode 100644
index 000000000..4f231d28a
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select-shared.component.scss
@@ -0,0 +1,20 @@
1$width-size: auto;
2
3ng-select {
4 width: $width-size;
5 @media screen and (max-width: $width-size) {
6 width: 100%;
7 }
8}
9
10// make sure the image is vertically adjusted
11ng-select ::ng-deep .ng-value-label img {
12 position: relative;
13 top: -1px;
14}
15
16ng-select ::ng-deep img {
17 border-radius: 50%;
18 height: 20px;
19 width: 20px;
20}
diff --git a/client/src/app/shared/shared-forms/select-tags.component.html b/client/src/app/shared/shared-forms/select-tags.component.html
new file mode 100644
index 000000000..0609c9d20
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select-tags.component.html
@@ -0,0 +1,13 @@
1<ng-select
2 [items]="items"
3 [(ngModel)]="_items"
4 (ngModelChange)="onModelChange()"
5 i18n-placeholder placeholder="Enter a new tag"
6 [maxSelectedItems]="5"
7 [clearable]="true"
8 [addTag]="true"
9 [multiple]="true"
10 [isOpen]="false"
11 [searchable]="true"
12>
13</ng-select>
diff --git a/client/src/app/shared/shared-forms/select-tags.component.scss b/client/src/app/shared/shared-forms/select-tags.component.scss
new file mode 100644
index 000000000..ad76bc7ee
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select-tags.component.scss
@@ -0,0 +1,3 @@
1ng-select ::ng-deep .ng-arrow-wrapper {
2 display: none;
3}
diff --git a/client/src/app/shared/shared-forms/select-tags.component.ts b/client/src/app/shared/shared-forms/select-tags.component.ts
new file mode 100644
index 000000000..2e07d7e8f
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select-tags.component.ts
@@ -0,0 +1,38 @@
1import { Component, Input, forwardRef } from '@angular/core'
2import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
3
4@Component({
5 selector: 'my-select-tags',
6 styleUrls: [ './select-shared.component.scss', './select-tags.component.scss' ],
7 templateUrl: './select-tags.component.html',
8 providers: [
9 {
10 provide: NG_VALUE_ACCESSOR,
11 useExisting: forwardRef(() => SelectTagsComponent),
12 multi: true
13 }
14 ]
15})
16export class SelectTagsComponent implements ControlValueAccessor {
17 @Input() items: string[] = []
18 @Input() _items: string[] = []
19
20 propagateChange = (_: any) => { /* empty */ }
21
22 writeValue (items: string[]) {
23 this._items = items
24 this.propagateChange(this._items)
25 }
26
27 registerOnChange (fn: (_: any) => void) {
28 this.propagateChange = fn
29 }
30
31 registerOnTouched () {
32 // Unused
33 }
34
35 onModelChange () {
36 this.propagateChange(this._items)
37 }
38}
diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts
index ba33704cf..19d812948 100644
--- a/client/src/app/shared/shared-forms/shared-form.module.ts
+++ b/client/src/app/shared/shared-forms/shared-form.module.ts
@@ -28,6 +28,9 @@ import { PreviewUploadComponent } from './preview-upload.component'
28import { ReactiveFileComponent } from './reactive-file.component' 28import { ReactiveFileComponent } from './reactive-file.component'
29import { TextareaAutoResizeDirective } from './textarea-autoresize.directive' 29import { TextareaAutoResizeDirective } from './textarea-autoresize.directive'
30import { TimestampInputComponent } from './timestamp-input.component' 30import { TimestampInputComponent } from './timestamp-input.component'
31import { SelectChannelComponent } from './select-channel.component'
32import { SelectOptionsComponent } from './select-options.component'
33import { SelectTagsComponent } from './select-tags.component'
31 34
32@NgModule({ 35@NgModule({
33 imports: [ 36 imports: [
@@ -45,7 +48,10 @@ import { TimestampInputComponent } from './timestamp-input.component'
45 PreviewUploadComponent, 48 PreviewUploadComponent,
46 ReactiveFileComponent, 49 ReactiveFileComponent,
47 TextareaAutoResizeDirective, 50 TextareaAutoResizeDirective,
48 TimestampInputComponent 51 TimestampInputComponent,
52 SelectChannelComponent,
53 SelectOptionsComponent,
54 SelectTagsComponent
49 ], 55 ],
50 56
51 exports: [ 57 exports: [
@@ -58,7 +64,10 @@ import { TimestampInputComponent } from './timestamp-input.component'
58 PreviewUploadComponent, 64 PreviewUploadComponent,
59 ReactiveFileComponent, 65 ReactiveFileComponent,
60 TextareaAutoResizeDirective, 66 TextareaAutoResizeDirective,
61 TimestampInputComponent 67 TimestampInputComponent,
68 SelectChannelComponent,
69 SelectOptionsComponent,
70 SelectTagsComponent
62 ], 71 ],
63 72
64 providers: [ 73 providers: [
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 22a207e51..a4d18d562 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -17,6 +17,7 @@ import {
17 NgbPopoverModule, 17 NgbPopoverModule,
18 NgbTooltipModule 18 NgbTooltipModule
19} from '@ng-bootstrap/ng-bootstrap' 19} from '@ng-bootstrap/ng-bootstrap'
20import { NgSelectModule } from '@ng-select/ng-select'
20import { I18n } from '@ngx-translate/i18n-polyfill' 21import { I18n } from '@ngx-translate/i18n-polyfill'
21import { SharedGlobalIconModule } from '../shared-icons' 22import { SharedGlobalIconModule } from '../shared-icons'
22import { AccountService, ActorAvatarInfoComponent, AvatarComponent } from './account' 23import { AccountService, ActorAvatarInfoComponent, AvatarComponent } from './account'
@@ -55,6 +56,8 @@ import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
55 MultiSelectModule, 56 MultiSelectModule,
56 InputSwitchModule, 57 InputSwitchModule,
57 58
59 NgSelectModule,
60
58 SharedGlobalIconModule 61 SharedGlobalIconModule
59 ], 62 ],
60 63
@@ -134,7 +137,9 @@ import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
134 TopMenuDropdownComponent, 137 TopMenuDropdownComponent,
135 138
136 UserQuotaComponent, 139 UserQuotaComponent,
137 UserNotificationsComponent 140 UserNotificationsComponent,
141
142 NgSelectModule
138 ], 143 ],
139 144
140 providers: [ 145 providers: [
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index edaefa9f2..978f775bf 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -339,23 +339,25 @@ export class VideoService implements VideosProvider {
339 const base = [ 339 const base = [
340 { 340 {
341 id: VideoPrivacy.PRIVATE, 341 id: VideoPrivacy.PRIVATE,
342 label: this.i18n('Only I can see this video') 342 description: this.i18n('Only I can see this video')
343 }, 343 },
344 { 344 {
345 id: VideoPrivacy.UNLISTED, 345 id: VideoPrivacy.UNLISTED,
346 label: this.i18n('Only people with the private link can see this video') 346 description: this.i18n('Only shareable via a private link')
347 }, 347 },
348 { 348 {
349 id: VideoPrivacy.PUBLIC, 349 id: VideoPrivacy.PUBLIC,
350 label: this.i18n('Anyone can see this video') 350 description: this.i18n('Anyone can see this video')
351 }, 351 },
352 { 352 {
353 id: VideoPrivacy.INTERNAL, 353 id: VideoPrivacy.INTERNAL,
354 label: this.i18n('Only users of this instance can see this video') 354 description: this.i18n('Only users of this instance can see this video')
355 } 355 }
356 ] 356 ]
357 357
358 return base.filter(o => !!privacies.find(p => p.id === o.id)) 358 return base
359 .filter(o => !!privacies.find(p => p.id === o.id)) // filter down to privacies that where in the input
360 .map(o => ({ ...privacies[o.id - 1], ...o })) // merge the input privacies that contain a label, and extend them with a description
359 } 361 }
360 362
361 nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) { 363 nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) {