aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/package.json5
-rw-r--r--client/src/app/+search/search-filters.component.html14
-rw-r--r--client/src/app/+search/search-filters.component.scss2
-rw-r--r--client/src/app/+search/search-filters.component.ts6
-rw-r--r--client/src/app/+search/search.module.ts4
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html10
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss5
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html59
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts38
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.module.ts3
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html16
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html16
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-send.scss7
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-send.ts4
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html17
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/video-update.resolver.ts7
-rw-r--r--client/src/app/helpers/utils.ts12
-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
-rw-r--r--client/src/sass/application.scss1
-rw-r--r--client/src/sass/include/_mixins.scss69
-rw-r--r--client/src/sass/ng-select.scss29
-rw-r--r--client/yarn.lock22
-rw-r--r--shared/models/videos/video-constant.model.ts1
36 files changed, 399 insertions, 217 deletions
diff --git a/client/package.json b/client/package.json
index 946cd109c..50b33d7c3 100644
--- a/client/package.json
+++ b/client/package.json
@@ -44,6 +44,7 @@
44 "@angularclass/hmr": "^2.1.3", 44 "@angularclass/hmr": "^2.1.3",
45 "@neos21/bootstrap3-glyphicons": "^1.0.1", 45 "@neos21/bootstrap3-glyphicons": "^1.0.1",
46 "@ng-bootstrap/ng-bootstrap": "^7.0.0", 46 "@ng-bootstrap/ng-bootstrap": "^7.0.0",
47 "@ng-select/ng-select": "^5.0.0",
47 "@ngx-i18nsupport/ngx-i18nsupport": "^1.1.6", 48 "@ngx-i18nsupport/ngx-i18nsupport": "^1.1.6",
48 "@ngx-i18nsupport/tooling": "^8.0.3", 49 "@ngx-i18nsupport/tooling": "^8.0.3",
49 "@ngx-loading-bar/core": "^5.0.0", 50 "@ngx-loading-bar/core": "^5.0.0",
@@ -95,7 +96,6 @@
95 "linkifyjs": "^2.1.5", 96 "linkifyjs": "^2.1.5",
96 "lodash-es": "^4.17.4", 97 "lodash-es": "^4.17.4",
97 "markdown-it": "^11.0.0", 98 "markdown-it": "^11.0.0",
98 "ngx-chips": "2.1.0",
99 "ngx-pipes": "^2.6.0", 99 "ngx-pipes": "^2.6.0",
100 "node-sass": "^4.9.3", 100 "node-sass": "^4.9.3",
101 "npm-font-source-sans-pro": "^1.0.2", 101 "npm-font-source-sans-pro": "^1.0.2",
@@ -133,6 +133,5 @@
133 "webtorrent": "^0.108.1", 133 "webtorrent": "^0.108.1",
134 "whatwg-fetch": "^3.0.0", 134 "whatwg-fetch": "^3.0.0",
135 "zone.js": "~0.10.2" 135 "zone.js": "~0.10.2"
136 }, 136 }
137 "dependencies": {}
138} 137}
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html
index e20aef8fb..b36b1d2ae 100644
--- a/client/src/app/+search/search-filters.component.html
+++ b/client/src/app/+search/search-filters.component.html
@@ -144,12 +144,7 @@
144 <button i18n class="reset-button reset-button-small" (click)="resetField('tagsAllOf')" *ngIf="advancedSearch.tagsAllOf"> 144 <button i18n class="reset-button reset-button-small" (click)="resetField('tagsAllOf')" *ngIf="advancedSearch.tagsAllOf">
145 Reset 145 Reset
146 </button> 146 </button>
147 <tag-input 147 <my-select-tags labelForId="tagsAllOf" id="tagsAllOf" [(ngModel)]="advancedSearch.tagsAllOf"></my-select-tags>
148 [(ngModel)]="advancedSearch.tagsAllOf" name="tagsAllOf" id="tagsAllOf"
149 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
150 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag"
151 [maxItems]="5" [modelAsStrings]="true"
152 ></tag-input>
153 </div> 148 </div>
154 149
155 <div class="form-group"> 150 <div class="form-group">
@@ -157,12 +152,7 @@
157 <button i18n class="reset-button reset-button-small" (click)="resetField('tagsOneOf')" *ngIf="advancedSearch.tagsOneOf"> 152 <button i18n class="reset-button reset-button-small" (click)="resetField('tagsOneOf')" *ngIf="advancedSearch.tagsOneOf">
158 Reset 153 Reset
159 </button> 154 </button>
160 <tag-input 155 <my-select-tags labelForId="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf"></my-select-tags>
161 [(ngModel)]="advancedSearch.tagsOneOf" name="tagsOneOf" id="tagsOneOf"
162 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
163 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag"
164 [maxItems]="5" [modelAsStrings]="true"
165 ></tag-input>
166 </div> 156 </div>
167 157
168 <div class="form-group" *ngIf="isSearchTargetEnabled()"> 158 <div class="form-group" *ngIf="isSearchTargetEnabled()">
diff --git a/client/src/app/+search/search-filters.component.scss b/client/src/app/+search/search-filters.component.scss
index a88a1c0b0..68ac6d021 100644
--- a/client/src/app/+search/search-filters.component.scss
+++ b/client/src/app/+search/search-filters.component.scss
@@ -65,5 +65,3 @@ input[type=submit] {
65 display: flex; 65 display: flex;
66 white-space: nowrap; 66 white-space: nowrap;
67} 67}
68
69@include ng2-tags;
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts
index fc1db3258..13ad61647 100644
--- a/client/src/app/+search/search-filters.component.ts
+++ b/client/src/app/+search/search-filters.component.ts
@@ -1,5 +1,4 @@
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { ValidatorFn } from '@angular/forms'
3import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
4import { VideoValidatorsService } from '@app/shared/shared-forms' 3import { VideoValidatorsService } from '@app/shared/shared-forms'
5import { AdvancedSearch } from '@app/shared/shared-search' 4import { AdvancedSearch } from '@app/shared/shared-search'
@@ -20,9 +19,6 @@ export class SearchFiltersComponent implements OnInit {
20 videoLicences: VideoConstant<number>[] = [] 19 videoLicences: VideoConstant<number>[] = []
21 videoLanguages: VideoConstant<string>[] = [] 20 videoLanguages: VideoConstant<string>[] = []
22 21
23 tagValidators: ValidatorFn[]
24 tagValidatorsMessages: { [ name: string ]: string }
25
26 publishedDateRanges: { id: string, label: string }[] = [] 22 publishedDateRanges: { id: string, label: string }[] = []
27 sorts: { id: string, label: string }[] = [] 23 sorts: { id: string, label: string }[] = []
28 durationRanges: { id: string, label: string }[] = [] 24 durationRanges: { id: string, label: string }[] = []
@@ -40,8 +36,6 @@ export class SearchFiltersComponent implements OnInit {
40 private videoValidatorsService: VideoValidatorsService, 36 private videoValidatorsService: VideoValidatorsService,
41 private serverService: ServerService 37 private serverService: ServerService
42 ) { 38 ) {
43 this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
44 this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
45 this.publishedDateRanges = [ 39 this.publishedDateRanges = [
46 { 40 {
47 id: 'any_published_date', 41 id: 'any_published_date',
diff --git a/client/src/app/+search/search.module.ts b/client/src/app/+search/search.module.ts
index ee4f07ad1..e85ae07d0 100644
--- a/client/src/app/+search/search.module.ts
+++ b/client/src/app/+search/search.module.ts
@@ -1,4 +1,3 @@
1import { TagInputModule } from 'ngx-chips'
2import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
3import { SharedFormModule } from '@app/shared/shared-forms' 2import { SharedFormModule } from '@app/shared/shared-forms'
4import { SharedMainModule } from '@app/shared/shared-main' 3import { SharedMainModule } from '@app/shared/shared-main'
@@ -14,8 +13,6 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
14 13
15@NgModule({ 14@NgModule({
16 imports: [ 15 imports: [
17 TagInputModule,
18
19 SearchRoutingModule, 16 SearchRoutingModule,
20 17
21 SharedMainModule, 18 SharedMainModule,
@@ -31,7 +28,6 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
31 ], 28 ],
32 29
33 exports: [ 30 exports: [
34 TagInputModule,
35 SearchComponent 31 SearchComponent
36 ], 32 ],
37 33
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html
index 6a9e31b5a..6a07dafa7 100644
--- a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html
@@ -8,11 +8,11 @@
8 8
9 <div class="modal-body"> 9 <div class="modal-body">
10 <label i18n for="language">Language</label> 10 <label i18n for="language">Language</label>
11 <div class="peertube-select-container"> 11 <div class="peertube-ng-select-container">
12 <select id="language" formControlName="language" class="form-control"> 12 <ng-select
13 <option></option> 13 labelForId="language" [items]="videoCaptionLanguages" formControlName="language"
14 <option *ngFor="let language of videoCaptionLanguages" [value]="language.id">{{ language.label }}</option> 14 bindLabel="label" bindValue="id"
15 </select> 15 ></ng-select>
16 </div> 16 </div>
17 17
18 <div *ngIf="formErrors.language" class="form-error"> 18 <div *ngIf="formErrors.language" class="form-error">
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss
index b257a16a9..0958b5f80 100644
--- a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss
@@ -1,8 +1,9 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.peertube-select-container { 4label {
5 @include peertube-select-container(auto); 5 font-weight: $font-regular;
6 font-size: 100%;
6} 7}
7 8
8.caption-file { 9.caption-file {
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index 4d3b84626..ae3413e79 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -28,11 +28,10 @@
28 </ng-template> 28 </ng-template>
29 </my-help> 29 </my-help>
30 30
31 <tag-input 31 <my-select-tags labelForId="label-tags" formControlName="tags"></my-select-tags>
32 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" 32 <div *ngIf="formErrors.tags" class="form-error">
33 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag" 33 {{ formErrors.tags }}
34 formControlName="tags" [maxItems]="5" [modelAsStrings]="true" 34 </div>
35 ></tag-input>
36 </div> 35 </div>
37 36
38 <div class="form-group"> 37 <div class="form-group">
@@ -56,22 +55,15 @@
56 55
57 <div class="col-video-edit"> 56 <div class="col-video-edit">
58 <div class="form-group"> 57 <div class="form-group">
59 <label i18n>Channel</label> 58 <label i18n for="channel">Channel</label>
60 <div class="peertube-select-container"> 59 <my-select-channel labelForId="channel" [items]="userVideoChannels" formControlName="channelId"></my-select-channel>
61 <select formControlName="channelId" class="form-control">
62 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
63 </select>
64 </div>
65 </div> 60 </div>
66 61
67 <div class="form-group"> 62 <div class="form-group">
68 <label i18n for="category">Category</label> 63 <label i18n for="category">Category</label>
69 <div class="peertube-select-container"> 64 <my-select-options
70 <select id="category" formControlName="category" class="form-control"> 65 labelForId="category" [items]="videoCategories" formControlName="category" [clearable]="true"
71 <option></option> 66 ></my-select-options>
72 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
73 </select>
74 </div>
75 67
76 <div *ngIf="formErrors.category" class="form-error"> 68 <div *ngIf="formErrors.category" class="form-error">
77 {{ formErrors.category }} 69 {{ formErrors.category }}
@@ -80,12 +72,9 @@
80 72
81 <div class="form-group"> 73 <div class="form-group">
82 <label i18n for="licence">Licence</label> 74 <label i18n for="licence">Licence</label>
83 <div class="peertube-select-container"> 75 <my-select-options
84 <select id="licence" formControlName="licence" class="form-control"> 76 labelForId="licence" [items]="videoLicences" formControlName="licence" [clearable]="true"
85 <option></option> 77 ></my-select-options>
86 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
87 </select>
88 </div>
89 78
90 <div *ngIf="formErrors.licence" class="form-error"> 79 <div *ngIf="formErrors.licence" class="form-error">
91 {{ formErrors.licence }} 80 {{ formErrors.licence }}
@@ -94,12 +83,10 @@
94 83
95 <div class="form-group"> 84 <div class="form-group">
96 <label i18n for="language">Language</label> 85 <label i18n for="language">Language</label>
97 <div class="peertube-select-container"> 86 <my-select-options
98 <select id="language" formControlName="language" class="form-control"> 87 labelForId="language" [items]="videoLanguages" formControlName="language"
99 <option></option> 88 [clearable]="true" [searchable]="true" [groupBy]="'group'"
100 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> 89 ></my-select-options>
101 </select>
102 </div>
103 90
104 <div *ngIf="formErrors.language" class="form-error"> 91 <div *ngIf="formErrors.language" class="form-error">
105 {{ formErrors.language }} 92 {{ formErrors.language }}
@@ -108,13 +95,9 @@
108 95
109 <div class="form-group"> 96 <div class="form-group">
110 <label i18n for="privacy">Privacy</label> 97 <label i18n for="privacy">Privacy</label>
111 <div class="peertube-select-container"> 98 <my-select-options
112 <select id="privacy" formControlName="privacy" class="form-control"> 99 labelForId="privacy" [items]="videoPrivacies" formControlName="privacy" [clearable]="false"
113 <option></option> 100 ></my-select-options>
114 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
115 <option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
116 </select>
117 </div>
118 101
119 <div *ngIf="formErrors.privacy" class="form-error"> 102 <div *ngIf="formErrors.privacy" class="form-error">
120 {{ formErrors.privacy }} 103 {{ formErrors.privacy }}
@@ -136,7 +119,7 @@
136 119
137 <my-peertube-checkbox inputName="nsfw" formControlName="nsfw" helpPlacement="bottom-right"> 120 <my-peertube-checkbox inputName="nsfw" formControlName="nsfw" helpPlacement="bottom-right">
138 <ng-template ptTemplate="label"> 121 <ng-template ptTemplate="label">
139 <ng-container i18n>This video contains mature or explicit content</ng-container> 122 <ng-container i18n>Contains sensitive content</ng-container>
140 </ng-template> 123 </ng-template>
141 124
142 <ng-template ptTemplate="help"> 125 <ng-template ptTemplate="help">
@@ -146,7 +129,7 @@
146 129
147 <my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right"> 130 <my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right">
148 <ng-template ptTemplate="label"> 131 <ng-template ptTemplate="label">
149 <ng-container i18n>Wait transcoding before publishing the video</ng-container> 132 <ng-container i18n>Publish after transcoding</ng-container>
150 </ng-template> 133 </ng-template>
151 134
152 <ng-template ptTemplate="help"> 135 <ng-template ptTemplate="help">
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
index 69b907288..9caf009c5 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
@@ -161,8 +161,6 @@ p-calendar {
161 } 161 }
162} 162}
163 163
164@include ng2-tags;
165
166// columns for the video 164// columns for the video
167.col-video-edit { 165.col-video-edit {
168 @include make-col-ready(); 166 @include make-col-ready();
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
index 239e453ad..4cd3838de 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
@@ -8,6 +8,11 @@ import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-ma
8import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' 8import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
9import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' 9import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
10import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 10import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { forkJoin } from 'rxjs'
13import { InstanceService } from '@app/shared/shared-instance'
14
15type VideoLanguages = VideoConstant<string> & { group?: string }
11 16
12@Component({ 17@Component({
13 selector: 'my-video-edit', 18 selector: 'my-video-edit',
@@ -31,7 +36,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
31 videoPrivacies: VideoConstant<VideoPrivacy>[] = [] 36 videoPrivacies: VideoConstant<VideoPrivacy>[] = []
32 videoCategories: VideoConstant<number>[] = [] 37 videoCategories: VideoConstant<number>[] = []
33 videoLicences: VideoConstant<number>[] = [] 38 videoLicences: VideoConstant<number>[] = []
34 videoLanguages: VideoConstant<string>[] = [] 39 videoLanguages: VideoLanguages[] = []
35 40
36 tagValidators: ValidatorFn[] 41 tagValidators: ValidatorFn[]
37 tagValidatorsMessages: { [ name: string ]: string } 42 tagValidatorsMessages: { [ name: string ]: string }
@@ -56,12 +61,11 @@ export class VideoEditComponent implements OnInit, OnDestroy {
56 private videoValidatorsService: VideoValidatorsService, 61 private videoValidatorsService: VideoValidatorsService,
57 private videoService: VideoService, 62 private videoService: VideoService,
58 private serverService: ServerService, 63 private serverService: ServerService,
64 private instanceService: InstanceService,
59 private i18nPrimengCalendarService: I18nPrimengCalendarService, 65 private i18nPrimengCalendarService: I18nPrimengCalendarService,
66 private i18n: I18n,
60 private ngZone: NgZone 67 private ngZone: NgZone
61 ) { 68 ) {
62 this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
63 this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
64
65 this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale() 69 this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
66 this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone() 70 this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
67 this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat() 71 this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
@@ -93,7 +97,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
93 licence: this.videoValidatorsService.VIDEO_LICENCE, 97 licence: this.videoValidatorsService.VIDEO_LICENCE,
94 language: this.videoValidatorsService.VIDEO_LANGUAGE, 98 language: this.videoValidatorsService.VIDEO_LANGUAGE,
95 description: this.videoValidatorsService.VIDEO_DESCRIPTION, 99 description: this.videoValidatorsService.VIDEO_DESCRIPTION,
96 tags: null, 100 tags: this.videoValidatorsService.VIDEO_TAGS_ARRAY,
97 previewfile: null, 101 previewfile: null,
98 support: this.videoValidatorsService.VIDEO_SUPPORT, 102 support: this.videoValidatorsService.VIDEO_SUPPORT,
99 schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, 103 schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT,
@@ -126,11 +130,29 @@ export class VideoEditComponent implements OnInit, OnDestroy {
126 .subscribe(res => this.videoCategories = res) 130 .subscribe(res => this.videoCategories = res)
127 this.serverService.getVideoLicences() 131 this.serverService.getVideoLicences()
128 .subscribe(res => this.videoLicences = res) 132 .subscribe(res => this.videoLicences = res)
129 this.serverService.getVideoLanguages() 133 forkJoin([
130 .subscribe(res => this.videoLanguages = res) 134 this.instanceService.getAbout(),
135 this.serverService.getVideoLanguages()
136 ]).pipe(map(([ about, languages ]) => ({ about, languages })))
137 .subscribe(res => {
138 this.videoLanguages = res.languages
139 .map(l => res.about.instance.languages.includes(l.id)
140 ? { ...l, group: this.i18n('Instance languages'), groupOrder: 0 }
141 : { ...l, group: this.i18n('All languages'), groupOrder: 1 })
142 .sort((a, b) => a.groupOrder - b.groupOrder)
143 })
131 144
132 this.serverService.getVideoPrivacies() 145 this.serverService.getVideoPrivacies()
133 .subscribe(privacies => this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies)) 146 .subscribe(privacies => {
147 this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies)
148 if (this.schedulePublicationPossible) {
149 this.videoPrivacies.push({
150 id: this.SPECIAL_SCHEDULED_PRIVACY,
151 label: this.i18n('Scheduled'),
152 description: this.i18n('Hide the video until a specific date')
153 })
154 }
155 })
134 156
135 this.serverConfig = this.serverService.getTmpConfig() 157 this.serverConfig = this.serverService.getTmpConfig()
136 this.serverService.getConfig() 158 this.serverService.getConfig()
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.module.ts b/client/src/app/+videos/+video-edit/shared/video-edit.module.ts
index d1bbbafe9..593114181 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.module.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.module.ts
@@ -1,4 +1,3 @@
1import { TagInputModule } from 'ngx-chips'
2import { CalendarModule } from 'primeng/calendar' 1import { CalendarModule } from 'primeng/calendar'
3import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
4import { SharedFormModule } from '@app/shared/shared-forms' 3import { SharedFormModule } from '@app/shared/shared-forms'
@@ -10,7 +9,6 @@ import { VideoEditComponent } from './video-edit.component'
10 9
11@NgModule({ 10@NgModule({
12 imports: [ 11 imports: [
13 TagInputModule,
14 CalendarModule, 12 CalendarModule,
15 13
16 SharedMainModule, 14 SharedMainModule,
@@ -24,7 +22,6 @@ import { VideoEditComponent } from './video-edit.component'
24 ], 22 ],
25 23
26 exports: [ 24 exports: [
27 TagInputModule,
28 CalendarModule, 25 CalendarModule,
29 26
30 SharedMainModule, 27 SharedMainModule,
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
index 5678f548f..8db37a293 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -25,20 +25,16 @@
25 25
26 <div class="form-group"> 26 <div class="form-group">
27 <label i18n for="first-step-channel">Channel</label> 27 <label i18n for="first-step-channel">Channel</label>
28 <div class="peertube-select-container"> 28 <my-select-channel
29 <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control"> 29 labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"
30 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> 30 ></my-select-channel>
31 </select>
32 </div>
33 </div> 31 </div>
34 32
35 <div class="form-group"> 33 <div class="form-group">
36 <label i18n for="first-step-privacy">Privacy</label> 34 <label i18n for="first-step-privacy">Privacy</label>
37 <div class="peertube-select-container"> 35 <my-select-options
38 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control"> 36 labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
39 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 37 ></my-select-options>
40 </select>
41 </div>
42 </div> 38 </div>
43 39
44 <input 40 <input
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
index 2e434271e..9b5cc3361 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
@@ -20,20 +20,16 @@
20 20
21 <div class="form-group"> 21 <div class="form-group">
22 <label i18n for="first-step-channel">Channel</label> 22 <label i18n for="first-step-channel">Channel</label>
23 <div class="peertube-select-container"> 23 <my-select-channel
24 <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control"> 24 labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"
25 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> 25 ></my-select-channel>
26 </select>
27 </div>
28 </div> 26 </div>
29 27
30 <div class="form-group"> 28 <div class="form-group">
31 <label i18n for="first-step-privacy">Privacy</label> 29 <label i18n for="first-step-privacy">Privacy</label>
32 <div class="peertube-select-container"> 30 <my-select-options
33 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control"> 31 labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
34 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 32 ></my-select-options>
35 </select>
36 </div>
37 </div> 33 </div>
38 34
39 <input 35 <input
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-send.scss b/client/src/app/+videos/+video-edit/video-add-components/video-send.scss
index cdb7c8280..17c5f63e9 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-send.scss
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-send.scss
@@ -26,6 +26,13 @@ $width-size: 190px;
26 .peertube-select-container { 26 .peertube-select-container {
27 @include peertube-select-container($width-size); 27 @include peertube-select-container($width-size);
28 } 28 }
29 my-select-options ::ng-deep ng-select,
30 my-select-channel ::ng-deep ng-select {
31 width: $width-size;
32 @media screen and (max-width: $width-size) {
33 width: 100%;
34 }
35 }
29 36
30 input[type=text] { 37 input[type=text] {
31 @include peertube-input-text($width-size); 38 @include peertube-input-text($width-size);
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts
index 2e658dfae..86f2b376f 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts
@@ -10,7 +10,7 @@ import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
10@Directive() 10@Directive()
11// tslint:disable-next-line: directive-class-suffix 11// tslint:disable-next-line: directive-class-suffix
12export abstract class VideoSend extends FormReactive implements OnInit { 12export abstract class VideoSend extends FormReactive implements OnInit {
13 userVideoChannels: { id: number, label: string, support: string }[] = [] 13 userVideoChannels: { id: number, label: string, support: string, avatarPath?: string }[] = []
14 videoPrivacies: VideoConstant<VideoPrivacy>[] = [] 14 videoPrivacies: VideoConstant<VideoPrivacy>[] = []
15 videoCaptions: VideoCaptionEdit[] = [] 15 videoCaptions: VideoCaptionEdit[] = []
16 16
@@ -44,7 +44,7 @@ export abstract class VideoSend extends FormReactive implements OnInit {
44 this.serverService.getVideoPrivacies() 44 this.serverService.getVideoPrivacies()
45 .subscribe( 45 .subscribe(
46 privacies => { 46 privacies => {
47 this.videoPrivacies = privacies 47 this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies)
48 48
49 this.firstStepPrivacyId = this.DEFAULT_VIDEO_PRIVACY 49 this.firstStepPrivacyId = this.DEFAULT_VIDEO_PRIVACY
50 }) 50 })
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
index 95b6628f6..ed697c25b 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
@@ -9,21 +9,16 @@
9 9
10 <div class="form-group form-group-channel"> 10 <div class="form-group form-group-channel">
11 <label i18n for="first-step-channel">Channel</label> 11 <label i18n for="first-step-channel">Channel</label>
12 <div class="peertube-select-container"> 12 <my-select-channel
13 <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control"> 13 labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"
14 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> 14 ></my-select-channel>
15 </select>
16 </div>
17 </div> 15 </div>
18 16
19 <div class="form-group"> 17 <div class="form-group">
20 <label i18n for="first-step-privacy">Privacy</label> 18 <label i18n for="first-step-privacy">Privacy</label>
21 <div class="peertube-select-container"> 19 <my-select-options
22 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control"> 20 labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
23 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 21 ></my-select-options>
24 <option i18n [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
25 </select>
26 </div>
27 </div> 22 </div>
28 23
29 <ng-container *ngIf="isUploadingAudioFile"> 24 <ng-container *ngIf="isUploadingAudioFile">
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
index 581199d65..de4f65df3 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -17,7 +17,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
17 video: VideoEdit 17 video: VideoEdit
18 18
19 isUpdatingVideo = false 19 isUpdatingVideo = false
20 userVideoChannels: { id: number, label: string, support: string }[] = [] 20 userVideoChannels: { id: number, label: string, support: string, avatar?: string }[] = []
21 schedulePublicationPossible = false 21 schedulePublicationPossible = false
22 videoCaptions: VideoCaptionEdit[] = [] 22 videoCaptions: VideoCaptionEdit[] = []
23 waitTranscodingEnabled = true 23 waitTranscodingEnabled = true
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts
index 30bcf4d74..a391913d8 100644
--- a/client/src/app/+videos/+video-edit/video-update.resolver.ts
+++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts
@@ -28,7 +28,12 @@ export class VideoUpdateResolver implements Resolve<any> {
28 .listAccountVideoChannels(video.account) 28 .listAccountVideoChannels(video.account)
29 .pipe( 29 .pipe(
30 map(result => result.data), 30 map(result => result.data),
31 map(videoChannels => videoChannels.map(c => ({ id: c.id, label: c.displayName, support: c.support }))) 31 map(videoChannels => videoChannels.map(c => ({
32 id: c.id,
33 label: c.displayName,
34 support: c.support,
35 avatarPath: c.avatar?.path
36 })))
32 ), 37 ),
33 38
34 this.videoCaptionService 39 this.videoCaptionService
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts
index 825b6ca96..aa37fdd46 100644
--- a/client/src/app/helpers/utils.ts
+++ b/client/src/app/helpers/utils.ts
@@ -16,7 +16,10 @@ function getParameterByName (name: string, url: string) {
16 return decodeURIComponent(results[2].replace(/\+/g, ' ')) 16 return decodeURIComponent(results[2].replace(/\+/g, ' '))
17} 17}
18 18
19function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support?: string }[]) { 19function populateAsyncUserVideoChannels (
20 authService: AuthService,
21 channel: { id: number, label: string, support?: string, avatarPath?: string, recent?: boolean }[]
22) {
20 return new Promise(res => { 23 return new Promise(res => {
21 authService.userInformationLoaded 24 authService.userInformationLoaded
22 .subscribe( 25 .subscribe(
@@ -27,7 +30,12 @@ function populateAsyncUserVideoChannels (authService: AuthService, channel: { id
27 const videoChannels = user.videoChannels 30 const videoChannels = user.videoChannels
28 if (Array.isArray(videoChannels) === false) return 31 if (Array.isArray(videoChannels) === false) return
29 32
30 videoChannels.forEach(c => channel.push({ id: c.id, label: c.displayName, support: c.support })) 33 videoChannels.forEach(c => channel.push({
34 id: c.id,
35 label: c.displayName,
36 support: c.support,
37 avatarPath: c.avatar?.path
38 }))
31 39
32 return res() 40 return res()
33 } 41 }
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) {
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index 30960393f..d2811d24a 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -13,6 +13,7 @@ $assets-path: '../../assets/';
13 13
14@import './bootstrap'; 14@import './bootstrap';
15@import './primeng-custom'; 15@import './primeng-custom';
16@import './ng-select.scss';
16 17
17[hidden] { 18[hidden] {
18 display: none !important; 19 display: none !important;
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index e4c2dffa0..ae2b99a5b 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -807,75 +807,6 @@
807 } 807 }
808} 808}
809 809
810@mixin ng2-tags {
811 ::ng-deep {
812 .ng2-tag-input {
813 border: none !important;
814 }
815
816 .ng2-tags-container {
817 display: flex;
818 align-items: center;
819 border: 1px solid #C6C6C6;
820 border-radius: 3px;
821 padding: 5px !important;
822 height: max-content;
823
824 &:focus-within {
825 box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
826 }
827 }
828
829 tag-input-form {
830 input {
831 height: 30px !important;
832 font-size: 12px !important;
833
834 background-color: pvar(--mainBackgroundColor) !important;
835 color: pvar(--mainForegroundColor) !important;
836 }
837 }
838
839 tag {
840 background-color: $grey-background-color !important;
841 color: #000 !important;
842 border-radius: 3px !important;
843 font-size: 12px !important;
844 height: 30px !important;
845 line-height: 30px !important;
846 margin: 0 5px 0 0 !important;
847 cursor: default !important;
848 padding: 0 8px 0 10px !important;
849
850 div {
851 height: 100% !important;
852 }
853 }
854
855 delete-icon {
856 cursor: pointer !important;
857 height: auto !important;
858 vertical-align: middle !important;
859 padding-left: 6px !important;
860
861 svg {
862 position: relative;
863 top: -1px;
864 height: auto !important;
865 vertical-align: middle !important;
866
867 path {
868 fill: pvar(--greyForegroundColor) !important;
869 }
870 }
871
872 &:hover {
873 transform: none !important;
874 }
875 }
876 }
877}
878
879@mixin divider($color: pvar(--submenuColor), $background: pvar(--mainBackgroundColor)) { 810@mixin divider($color: pvar(--submenuColor), $background: pvar(--mainBackgroundColor)) {
880 width: 95%; 811 width: 95%;
881 border-top: .05rem solid $color; 812 border-top: .05rem solid $color;
diff --git a/client/src/sass/ng-select.scss b/client/src/sass/ng-select.scss
new file mode 100644
index 000000000..f836e203c
--- /dev/null
+++ b/client/src/sass/ng-select.scss
@@ -0,0 +1,29 @@
1@import '_variables';
2
3$ng-select-highlight: #f2690d;
4// $ng-select-primary-text: #333 !default;
5// $ng-select-disabled-text: #f9f9f9 !default;
6// $ng-select-border: #ccc !default;
7// $ng-select-border-radius: 4px !default;
8// $ng-select-bg: #ffffff !default;
9// $ng-select-selected: lighten($ng-select-highlight, 46) !default;
10// $ng-select-marke d: lighten($ng-select-highlight, 48) !default;
11$ng-select-box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
12// $ng-select-placeholder: lighten($ng-select-primary-text, 40) !default;
13$ng-select-height: 30px;
14// $ng-select-value-padding-left: 10px !default;
15// $ng-select-value-font-size: 0.9em !default;
16
17@import "~@ng-select/ng-select/scss/default.theme.scss";
18
19.ng-input {
20 font-size: .9em;
21}
22
23.ng-select {
24 &.ng-select-focused {
25 &:not(.ng-select-opened) > .ng-select-container {
26 border-color: #ccc !important;
27 }
28 }
29}
diff --git a/client/yarn.lock b/client/yarn.lock
index 843ca4f10..38aea725e 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -1344,6 +1344,13 @@
1344 dependencies: 1344 dependencies:
1345 tslib "^2.0.0" 1345 tslib "^2.0.0"
1346 1346
1347"@ng-select/ng-select@^5.0.0":
1348 version "5.0.0"
1349 resolved "https://registry.yarnpkg.com/@ng-select/ng-select/-/ng-select-5.0.0.tgz#db021e5ca7be3fe6be346c3b63b3f2e109afa0f0"
1350 integrity sha512-1mdGNh5xGriSnCLcLF/GWxqCgCehB3Stu7mxhB9K/+BxNCRE365Q1MgpzO61XQBgL0L6ktxiXRhCQWXXtFoq9w==
1351 dependencies:
1352 tslib "^2.0.0"
1353
1347"@ngtools/webpack@10.1.0-next.4": 1354"@ngtools/webpack@10.1.0-next.4":
1348 version "10.1.0-next.4" 1355 version "10.1.0-next.4"
1349 resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-10.1.0-next.4.tgz#5397417820d110e29c7dc99db8adb538de98676e" 1356 resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-10.1.0-next.4.tgz#5397417820d110e29c7dc99db8adb538de98676e"
@@ -7866,21 +7873,6 @@ next-tick@~1.0.0:
7866 resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" 7873 resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
7867 integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= 7874 integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
7868 7875
7869ng2-material-dropdown@0.11.0:
7870 version "0.11.0"
7871 resolved "https://registry.yarnpkg.com/ng2-material-dropdown/-/ng2-material-dropdown-0.11.0.tgz#27a402ef3cbdcaf6791ef4cfd4b257e31db7546f"
7872 integrity sha512-wptBo09qKecY0QPTProAThrc4A3ajJTcHE9LTpCG5XZZUhXLBzhnGK8OW33TN8A+K/jqcs7OB74ppYJiqs3nhQ==
7873 dependencies:
7874 tslib "^1.9.0"
7875
7876ngx-chips@2.1.0:
7877 version "2.1.0"
7878 resolved "https://registry.yarnpkg.com/ngx-chips/-/ngx-chips-2.1.0.tgz#aa299bcf40dc3e1f6288bf1d29e2fdfe9a132ed3"
7879 integrity sha512-OQV4dTfD3nXm5d2mGKUSgwOtJOaMnZ4F+lwXOtd7DWRSUne0JQWwoZNHdOpuS6saBGhqCPDAwq6KxdR5XSgZUQ==
7880 dependencies:
7881 ng2-material-dropdown "0.11.0"
7882 tslib "^1.9.0"
7883
7884ngx-pipes@^2.6.0: 7876ngx-pipes@^2.6.0:
7885 version "2.7.5" 7877 version "2.7.5"
7886 resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.7.5.tgz#22e2e4b7015ae9103210dfa2dacd6f4ae4411639" 7878 resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.7.5.tgz#22e2e4b7015ae9103210dfa2dacd6f4ae4411639"
diff --git a/shared/models/videos/video-constant.model.ts b/shared/models/videos/video-constant.model.ts
index 342a7c0cf..353a29535 100644
--- a/shared/models/videos/video-constant.model.ts
+++ b/shared/models/videos/video-constant.model.ts
@@ -1,4 +1,5 @@
1export interface VideoConstant<T> { 1export interface VideoConstant<T> {
2 id: T 2 id: T
3 label: string 3 label: string
4 description?: string
4} 5}