diff options
Diffstat (limited to 'client/src/app/videos')
49 files changed, 1063 insertions, 1832 deletions
diff --git a/client/src/app/videos/shared/video-description.component.html b/client/src/app/videos/+video-edit/shared/video-description.component.html index 7a228857c..5d05467be 100644 --- a/client/src/app/videos/shared/video-description.component.html +++ b/client/src/app/videos/+video-edit/shared/video-description.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <textarea | 1 | <textarea |
2 | [(ngModel)]="description" (ngModelChange)="onModelChange()" | 2 | [(ngModel)]="description" (ngModelChange)="onModelChange()" |
3 | id="description" class="form-control" placeholder="My super video"> | 3 | id="description" name="description"> |
4 | </textarea> | 4 | </textarea> |
5 | 5 | ||
6 | <tabset #staticTabs class="previews"> | 6 | <tabset #staticTabs class="previews"> |
diff --git a/client/src/app/videos/+video-edit/shared/video-description.component.scss b/client/src/app/videos/+video-edit/shared/video-description.component.scss new file mode 100644 index 000000000..2a4c8d189 --- /dev/null +++ b/client/src/app/videos/+video-edit/shared/video-description.component.scss | |||
@@ -0,0 +1,24 @@ | |||
1 | textarea { | ||
2 | @include peertube-input-text(100%); | ||
3 | |||
4 | padding: 5px 15px; | ||
5 | font-size: 15px; | ||
6 | height: 150px; | ||
7 | margin-bottom: 15px; | ||
8 | } | ||
9 | |||
10 | /deep/ { | ||
11 | .nav-link { | ||
12 | display: flex !important; | ||
13 | align-items: center; | ||
14 | height: 30px !important; | ||
15 | padding: 0 15px !important; | ||
16 | } | ||
17 | |||
18 | .tab-content { | ||
19 | min-height: 75px; | ||
20 | padding: 15px; | ||
21 | font-size: 15px; | ||
22 | } | ||
23 | } | ||
24 | |||
diff --git a/client/src/app/videos/shared/video-description.component.ts b/client/src/app/videos/+video-edit/shared/video-description.component.ts index d9ffb7800..9b77a27e6 100644 --- a/client/src/app/videos/shared/video-description.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-description.component.ts | |||
@@ -1,12 +1,10 @@ | |||
1 | import { Component, forwardRef, Input, OnInit } from '@angular/core' | 1 | import { Component, forwardRef, Input, OnInit } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Subject } from 'rxjs/Subject' | 3 | import { truncate } from 'lodash' |
4 | import 'rxjs/add/operator/debounceTime' | 4 | import 'rxjs/add/operator/debounceTime' |
5 | import 'rxjs/add/operator/distinctUntilChanged' | 5 | import 'rxjs/add/operator/distinctUntilChanged' |
6 | 6 | import { Subject } from 'rxjs/Subject' | |
7 | import { truncate } from 'lodash' | 7 | import { MarkdownService } from '../../shared' |
8 | |||
9 | import { MarkdownService } from './markdown.service' | ||
10 | 8 | ||
11 | @Component({ | 9 | @Component({ |
12 | selector: 'my-video-description', | 10 | selector: 'my-video-description', |
@@ -62,6 +60,8 @@ export class VideoDescriptionComponent implements ControlValueAccessor, OnInit { | |||
62 | } | 60 | } |
63 | 61 | ||
64 | private updateDescriptionPreviews () { | 62 | private updateDescriptionPreviews () { |
63 | if (!this.description) return | ||
64 | |||
65 | this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 })) | 65 | this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 })) |
66 | this.descriptionHTML = this.markdownService.markdownToHTML(this.description) | 66 | this.descriptionHTML = this.markdownService.markdownToHTML(this.description) |
67 | } | 67 | } |
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 new file mode 100644 index 000000000..8c071ce12 --- /dev/null +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html | |||
@@ -0,0 +1,86 @@ | |||
1 | <div class="video-edit row" [formGroup]="form"> | ||
2 | |||
3 | <div class="col-md-8"> | ||
4 | <div class="form-group"> | ||
5 | <label for="name">Title</label> | ||
6 | <input type="text" id="name" formControlName="name" /> | ||
7 | <div *ngIf="formErrors.name" class="form-error"> | ||
8 | {{ formErrors.name }} | ||
9 | </div> | ||
10 | </div> | ||
11 | |||
12 | <div class="form-group"> | ||
13 | <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span> | ||
14 | <tag-input | ||
15 | [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | ||
16 | formControlName="tags" maxItems="5" modelAsStrings="true" | ||
17 | ></tag-input> | ||
18 | </div> | ||
19 | |||
20 | <div class="form-group"> | ||
21 | <label for="description">Description</label> | ||
22 | <my-video-description formControlName="description"></my-video-description> | ||
23 | |||
24 | <div *ngIf="formErrors.description" class="form-error"> | ||
25 | {{ formErrors.description }} | ||
26 | </div> | ||
27 | </div> | ||
28 | </div> | ||
29 | |||
30 | <div class="col-md-4"> | ||
31 | <div class="form-group"> | ||
32 | <label for="category">Category</label> | ||
33 | <select id="category" formControlName="category"> | ||
34 | <option></option> | ||
35 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> | ||
36 | </select> | ||
37 | |||
38 | <div *ngIf="formErrors.category" class="form-error"> | ||
39 | {{ formErrors.category }} | ||
40 | </div> | ||
41 | </div> | ||
42 | |||
43 | <div class="form-group"> | ||
44 | <label for="licence">Licence</label> | ||
45 | <select id="licence" formControlName="licence"> | ||
46 | <option></option> | ||
47 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> | ||
48 | </select> | ||
49 | |||
50 | <div *ngIf="formErrors.licence" class="form-error"> | ||
51 | {{ formErrors.licence }} | ||
52 | </div> | ||
53 | </div> | ||
54 | |||
55 | <div class="form-group"> | ||
56 | <label for="language">Language</label> | ||
57 | <select id="language" formControlName="language"> | ||
58 | <option></option> | ||
59 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> | ||
60 | </select> | ||
61 | |||
62 | <div *ngIf="formErrors.language" class="form-error"> | ||
63 | {{ formErrors.language }} | ||
64 | </div> | ||
65 | </div> | ||
66 | |||
67 | <div class="form-group"> | ||
68 | <label for="privacy">Privacy</label> | ||
69 | <select id="privacy" formControlName="privacy"> | ||
70 | |||
71 | <option></option> | ||
72 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | ||
73 | </select> | ||
74 | |||
75 | <div *ngIf="formErrors.privacy" class="form-error"> | ||
76 | {{ formErrors.privacy }} | ||
77 | </div> | ||
78 | </div> | ||
79 | |||
80 | <div class="form-group form-group-checkbox"> | ||
81 | <input type="checkbox" id="nsfw" formControlName="nsfw" /> | ||
82 | <label for="nsfw">This video contains mature or explicit content</label> | ||
83 | </div> | ||
84 | |||
85 | </div> | ||
86 | </div> | ||
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 9ee0c520c..d363499ce 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 | |||
@@ -1,48 +1,126 @@ | |||
1 | .btn-file { | 1 | .video-edit { |
2 | position: relative; | 2 | height: 100%; |
3 | overflow: hidden; | 3 | |
4 | display: block; | 4 | .form-group { |
5 | margin-bottom: 25px; | ||
6 | } | ||
7 | |||
8 | input { | ||
9 | @include peertube-input-text(100%); | ||
10 | display: block; | ||
11 | |||
12 | &[type=checkbox] { | ||
13 | outline: 0; | ||
14 | } | ||
15 | } | ||
16 | |||
17 | select { | ||
18 | @include peertube-select(100%); | ||
19 | } | ||
20 | |||
21 | input, select { | ||
22 | font-size: 15px | ||
23 | } | ||
24 | |||
25 | .form-group-checkbox { | ||
26 | display: flex; | ||
27 | align-items: center; | ||
28 | |||
29 | label { | ||
30 | font-weight: $font-regular; | ||
31 | margin: 0; | ||
32 | } | ||
33 | |||
34 | input { | ||
35 | width: 10px; | ||
36 | margin-right: 10px; | ||
37 | } | ||
38 | } | ||
5 | } | 39 | } |
6 | 40 | ||
7 | .btn-file input[type=file] { | 41 | .submit-container { |
8 | position: absolute; | ||
9 | top: 0; | ||
10 | right: 0; | ||
11 | min-width: 100%; | ||
12 | min-height: 100%; | ||
13 | font-size: 100px; | ||
14 | text-align: right; | 42 | text-align: right; |
15 | filter: alpha(opacity=0); | 43 | position: relative; |
16 | opacity: 0; | 44 | bottom: $button-height; |
17 | outline: none; | ||
18 | background: white; | ||
19 | cursor: inherit; | ||
20 | display: block; | ||
21 | } | ||
22 | 45 | ||
23 | .form-group { | 46 | .message-submit { |
24 | margin-bottom: 10px; | 47 | display: inline-block; |
25 | } | 48 | margin-right: 25px; |
49 | |||
50 | color: #585858; | ||
51 | font-size: 15px; | ||
52 | } | ||
53 | |||
54 | .submit-button { | ||
55 | @include peertube-button; | ||
56 | @include orange-button; | ||
57 | |||
58 | display: inline-block; | ||
26 | 59 | ||
27 | div.tags { | 60 | input { |
28 | height: 40px; | 61 | cursor: inherit; |
29 | font-size: 20px; | 62 | background-color: inherit; |
30 | margin-top: 20px; | 63 | border: none; |
64 | padding: 0; | ||
65 | outline: 0; | ||
66 | } | ||
31 | 67 | ||
32 | .tag { | 68 | .icon.icon-validate { |
33 | margin-right: 10px; | 69 | @include icon(20px); |
34 | 70 | ||
35 | .remove { | 71 | cursor: inherit; |
36 | cursor: pointer; | 72 | position: relative; |
73 | top: -1px; | ||
74 | margin-right: 4px; | ||
75 | background-image: url('../../../../assets/images/global/validate.svg'); | ||
37 | } | 76 | } |
38 | } | 77 | } |
39 | } | 78 | } |
40 | 79 | ||
41 | div.file-to-upload { | 80 | /deep/ { |
42 | height: 40px; | 81 | .ng2-tag-input { |
82 | border: none !important; | ||
83 | } | ||
43 | 84 | ||
44 | .glyphicon-remove { | 85 | .ng2-tags-container { |
45 | cursor: pointer; | 86 | display: flex; |
87 | align-items: center; | ||
88 | border: 1px solid #C6C6C6; | ||
89 | border-radius: 3px; | ||
90 | padding: 5px !important; | ||
91 | } | ||
92 | |||
93 | tag { | ||
94 | background-color: #E5E5E5 !important; | ||
95 | border-radius: 3px !important; | ||
96 | font-size: 15px !important; | ||
97 | color: #000 !important; | ||
98 | height: 30px !important; | ||
99 | line-height: 30px !important; | ||
100 | margin: 0 5px 0 0 !important; | ||
101 | cursor: default !important; | ||
102 | padding: 0 8px 0 10px !important; | ||
103 | |||
104 | div { | ||
105 | height: 100% !important; | ||
106 | } | ||
107 | } | ||
108 | |||
109 | delete-icon { | ||
110 | cursor: pointer !important; | ||
111 | height: auto !important; | ||
112 | vertical-align: middle !important; | ||
113 | padding-left: 6px !important; | ||
114 | |||
115 | svg { | ||
116 | height: auto !important; | ||
117 | vertical-align: middle !important; | ||
118 | fill: #585858 !important; | ||
119 | } | ||
120 | |||
121 | &:hover { | ||
122 | transform: none !important; | ||
123 | } | ||
46 | } | 124 | } |
47 | } | 125 | } |
48 | 126 | ||
@@ -50,7 +128,3 @@ div.file-to-upload { | |||
50 | font-size: 0.8em; | 128 | font-size: 0.8em; |
51 | font-style: italic; | 129 | font-style: italic; |
52 | } | 130 | } |
53 | |||
54 | .label-tags { | ||
55 | margin-bottom: 0; | ||
56 | } | ||
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 new file mode 100644 index 000000000..5b1cc3f9c --- /dev/null +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts | |||
@@ -0,0 +1,83 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { FormBuilder, FormControl, FormGroup } from '@angular/forms' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { ServerService } from 'app/core' | ||
6 | import { VideoEdit } from 'app/shared/video/video-edit.model' | ||
7 | import 'rxjs/add/observable/forkJoin' | ||
8 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' | ||
9 | import { | ||
10 | ValidatorMessage, | ||
11 | VIDEO_CATEGORY, | ||
12 | VIDEO_DESCRIPTION, | ||
13 | VIDEO_LANGUAGE, | ||
14 | VIDEO_LICENCE, | ||
15 | VIDEO_NAME, | ||
16 | VIDEO_PRIVACY, | ||
17 | VIDEO_TAGS | ||
18 | } from '../../../shared/forms/form-validators' | ||
19 | |||
20 | @Component({ | ||
21 | selector: 'my-video-edit', | ||
22 | styleUrls: [ './video-edit.component.scss' ], | ||
23 | templateUrl: './video-edit.component.html' | ||
24 | }) | ||
25 | |||
26 | export class VideoEditComponent implements OnInit { | ||
27 | @Input() form: FormGroup | ||
28 | @Input() formErrors: { [ id: string ]: string } = {} | ||
29 | @Input() validationMessages: ValidatorMessage = {} | ||
30 | @Input() videoPrivacies = [] | ||
31 | |||
32 | tags: string[] = [] | ||
33 | videoCategories = [] | ||
34 | videoLicences = [] | ||
35 | videoLanguages = [] | ||
36 | video: VideoEdit | ||
37 | |||
38 | tagValidators = VIDEO_TAGS.VALIDATORS | ||
39 | tagValidatorsMessages = VIDEO_TAGS.MESSAGES | ||
40 | |||
41 | error: string = null | ||
42 | |||
43 | constructor ( | ||
44 | private formBuilder: FormBuilder, | ||
45 | private route: ActivatedRoute, | ||
46 | private router: Router, | ||
47 | private notificationsService: NotificationsService, | ||
48 | private serverService: ServerService | ||
49 | ) { } | ||
50 | |||
51 | updateForm () { | ||
52 | this.formErrors['name'] = '' | ||
53 | this.formErrors['privacy'] = '' | ||
54 | this.formErrors['category'] = '' | ||
55 | this.formErrors['licence'] = '' | ||
56 | this.formErrors['language'] = '' | ||
57 | this.formErrors['description'] = '' | ||
58 | |||
59 | this.validationMessages['name'] = VIDEO_NAME.MESSAGES | ||
60 | this.validationMessages['privacy'] = VIDEO_PRIVACY.MESSAGES | ||
61 | this.validationMessages['category'] = VIDEO_CATEGORY.MESSAGES | ||
62 | this.validationMessages['licence'] = VIDEO_LICENCE.MESSAGES | ||
63 | this.validationMessages['language'] = VIDEO_LANGUAGE.MESSAGES | ||
64 | this.validationMessages['description'] = VIDEO_DESCRIPTION.MESSAGES | ||
65 | |||
66 | this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS)) | ||
67 | this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS)) | ||
68 | this.form.addControl('nsfw', new FormControl(false)) | ||
69 | this.form.addControl('category', new FormControl('', VIDEO_CATEGORY.VALIDATORS)) | ||
70 | this.form.addControl('licence', new FormControl('', VIDEO_LICENCE.VALIDATORS)) | ||
71 | this.form.addControl('language', new FormControl('', VIDEO_LANGUAGE.VALIDATORS)) | ||
72 | this.form.addControl('description', new FormControl('', VIDEO_DESCRIPTION.VALIDATORS)) | ||
73 | this.form.addControl('tags', new FormControl('')) | ||
74 | } | ||
75 | |||
76 | ngOnInit () { | ||
77 | this.updateForm() | ||
78 | |||
79 | this.videoCategories = this.serverService.getVideoCategories() | ||
80 | this.videoLicences = this.serverService.getVideoLicences() | ||
81 | this.videoLanguages = this.serverService.getVideoLanguages() | ||
82 | } | ||
83 | } | ||
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 c64cea920..ce106d82f 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 | |||
@@ -3,8 +3,10 @@ import { NgModule } from '@angular/core' | |||
3 | import { TagInputModule } from 'ngx-chips' | 3 | import { TagInputModule } from 'ngx-chips' |
4 | import { TabsModule } from 'ngx-bootstrap/tabs' | 4 | import { TabsModule } from 'ngx-bootstrap/tabs' |
5 | 5 | ||
6 | import { VideoService, MarkdownService, VideoDescriptionComponent } from '../../shared' | 6 | import { MarkdownService } from '../../shared' |
7 | import { SharedModule } from '../../../shared' | 7 | import { SharedModule } from '../../../shared' |
8 | import { VideoDescriptionComponent } from './video-description.component' | ||
9 | import { VideoEditComponent } from './video-edit.component' | ||
8 | 10 | ||
9 | @NgModule({ | 11 | @NgModule({ |
10 | imports: [ | 12 | imports: [ |
@@ -15,18 +17,19 @@ import { SharedModule } from '../../../shared' | |||
15 | ], | 17 | ], |
16 | 18 | ||
17 | declarations: [ | 19 | declarations: [ |
18 | VideoDescriptionComponent | 20 | VideoDescriptionComponent, |
21 | VideoEditComponent | ||
19 | ], | 22 | ], |
20 | 23 | ||
21 | exports: [ | 24 | exports: [ |
22 | TagInputModule, | 25 | TagInputModule, |
23 | TabsModule, | 26 | TabsModule, |
24 | 27 | ||
25 | VideoDescriptionComponent | 28 | VideoDescriptionComponent, |
29 | VideoEditComponent | ||
26 | ], | 30 | ], |
27 | 31 | ||
28 | providers: [ | 32 | providers: [ |
29 | VideoService, | ||
30 | MarkdownService | 33 | MarkdownService |
31 | ] | 34 | ] |
32 | }) | 35 | }) |
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html index b4e0f9f7c..a6f2bf6f2 100644 --- a/client/src/app/videos/+video-edit/video-add.component.html +++ b/client/src/app/videos/+video-edit/video-add.component.html | |||
@@ -1,141 +1,53 @@ | |||
1 | <div class="row"> | 1 | <div class="margin-content"> |
2 | <div class="content-padding"> | 2 | <div class="title-page title-page-single"> |
3 | Upload your video | ||
4 | </div> | ||
3 | 5 | ||
4 | <h3>Upload a video</h3> | 6 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> |
5 | 7 | ||
6 | <div *ngIf="error !== undefined" class="alert alert-danger">{{ error }}</div> | 8 | <div *ngIf="!isUploadingVideo" class="upload-video-container"> |
9 | <div class="upload-video"> | ||
10 | <div class="icon icon-upload"></div> | ||
7 | 11 | ||
8 | <form novalidate [formGroup]="form"> | 12 | <div class="button-file"> |
9 | <div class="form-group"> | 13 | <span>Select the file to upload</span> |
10 | <label for="name">Name</label> | 14 | <input #videofileInput type="file" name="videofile" id="videofile" (change)="fileChange()" /> |
11 | <input | ||
12 | type="text" class="form-control" id="name" | ||
13 | formControlName="name" | ||
14 | > | ||
15 | <div *ngIf="formErrors.name" class="alert alert-danger"> | ||
16 | {{ formErrors.name }} | ||
17 | </div> | ||
18 | </div> | 15 | </div> |
19 | 16 | ||
20 | <div class="form-group"> | 17 | <div class="form-group"> |
21 | <label for="privacy">Privacy</label> | 18 | <select [(ngModel)]="firstStepPrivacyId"> |
22 | <select class="form-control" id="privacy" formControlName="privacy"> | ||
23 | <option></option> | ||
24 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | 19 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> |
25 | </select> | 20 | </select> |
26 | |||
27 | <div *ngIf="formErrors.privacy" class="alert alert-danger"> | ||
28 | {{ formErrors.privacy }} | ||
29 | </div> | ||
30 | </div> | ||
31 | |||
32 | <div class="form-group"> | ||
33 | <input | ||
34 | type="checkbox" id="nsfw" | ||
35 | formControlName="nsfw" | ||
36 | > | ||
37 | <label for="nsfw">This video contains mature or explicit content</label> | ||
38 | </div> | 21 | </div> |
39 | 22 | ||
40 | <div class="form-group"> | 23 | <div class="form-group"> |
41 | <label for="category">Channel</label> | 24 | <select [(ngModel)]="firstStepChannelId"> |
42 | <select class="form-control" id="channelId" formControlName="channelId"> | ||
43 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> | 25 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> |
44 | </select> | 26 | </select> |
45 | |||
46 | <div *ngIf="formErrors.channelId" class="alert alert-danger"> | ||
47 | {{ formErrors.channelId }} | ||
48 | </div> | ||
49 | </div> | ||
50 | |||
51 | <div class="form-group"> | ||
52 | <label for="category">Category</label> | ||
53 | <select class="form-control" id="category" formControlName="category"> | ||
54 | <option></option> | ||
55 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> | ||
56 | </select> | ||
57 | |||
58 | <div *ngIf="formErrors.category" class="alert alert-danger"> | ||
59 | {{ formErrors.category }} | ||
60 | </div> | ||
61 | </div> | ||
62 | |||
63 | <div class="form-group"> | ||
64 | <label for="licence">Licence</label> | ||
65 | <select class="form-control" id="licence" formControlName="licence"> | ||
66 | <option></option> | ||
67 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> | ||
68 | </select> | ||
69 | |||
70 | <div *ngIf="formErrors.licence" class="alert alert-danger"> | ||
71 | {{ formErrors.licence }} | ||
72 | </div> | ||
73 | </div> | ||
74 | |||
75 | <div class="form-group"> | ||
76 | <label for="language">Language</label> | ||
77 | <select class="form-control" id="language" formControlName="language"> | ||
78 | <option></option> | ||
79 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> | ||
80 | </select> | ||
81 | |||
82 | <div *ngIf="formErrors.language" class="alert alert-danger"> | ||
83 | {{ formErrors.language }} | ||
84 | </div> | ||
85 | </div> | ||
86 | |||
87 | <div class="form-group"> | ||
88 | <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span> | ||
89 | <tag-input | ||
90 | [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | ||
91 | formControlName="tags" maxItems="5" modelAsStrings="true" | ||
92 | ></tag-input> | ||
93 | </div> | ||
94 | |||
95 | <div class="form-group"> | ||
96 | <label for="videofile">File</label> | ||
97 | <div class="btn btn-default btn-file"> | ||
98 | <span>Select the video...</span> | ||
99 | <input #videofileInput type="file" name="videofile" id="videofile" (change)="fileChange($event)" /> | ||
100 | <input type="hidden" name="videofileHidden" formControlName="videofile"/> | ||
101 | </div> | ||
102 | </div> | 27 | </div> |
28 | </div> | ||
29 | </div> | ||
103 | 30 | ||
104 | <div class="file-to-upload"> | 31 | <p-progressBar |
105 | <div class="file" *ngIf="filename"> | 32 | *ngIf="isUploadingVideo" [value]="videoUploadPercents" |
106 | <span class="filename">{{ filename }}</span> | 33 | [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }" |
107 | <span class="glyphicon glyphicon-remove" (click)="removeFile()"></span> | 34 | ></p-progressBar> |
108 | </div> | ||
109 | </div> | ||
110 | 35 | ||
111 | <div *ngIf="formErrors.videofile" class="alert alert-danger"> | 36 | <!-- Hidden because we need to load the component --> |
112 | {{ formErrors.videofile }} | 37 | <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> |
113 | </div> | 38 | <my-video-edit |
39 | [form]="form" [formErrors]="formErrors" | ||
40 | [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" | ||
41 | ></my-video-edit> | ||
114 | 42 | ||
115 | <div class="form-group"> | ||
116 | <label for="description">Description</label> | ||
117 | <my-video-description formControlName="description"></my-video-description> | ||
118 | 43 | ||
119 | <div *ngIf="formErrors.description" class="alert alert-danger"> | 44 | <div class="submit-container"> |
120 | {{ formErrors.description }} | 45 | <div *ngIf="videoUploaded === false" class="message-submit">Publish will be available when upload is finished</div> |
121 | </div> | ||
122 | </div> | ||
123 | 46 | ||
124 | <div class="progress"> | 47 | <div class="submit-button" (click)="updateSecondStep()" [ngClass]="{ disabled: !form.valid || videoUploaded !== true }"> |
125 | <progressbar [value]="progressPercent" max="100"> | 48 | <span class="icon icon-validate"></span> |
126 | <ng-template [ngIf]="progressPercent === 100"> | 49 | <input type="button" value="Publish" /> |
127 | <span class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span> | ||
128 | Server is processing the video | ||
129 | </ng-template> | ||
130 | </progressbar> | ||
131 | </div> | 50 | </div> |
132 | 51 | </div> | |
133 | <div class="form-group"> | 52 | </form> |
134 | <input | ||
135 | type="button" value="Upload" class="btn btn-default form-control" | ||
136 | (click)="upload()" | ||
137 | > | ||
138 | </div> | ||
139 | </form> | ||
140 | </div> | ||
141 | </div> | 53 | </div> |
diff --git a/client/src/app/videos/+video-edit/video-add.component.scss b/client/src/app/videos/+video-edit/video-add.component.scss new file mode 100644 index 000000000..39673b4b7 --- /dev/null +++ b/client/src/app/videos/+video-edit/video-add.component.scss | |||
@@ -0,0 +1,96 @@ | |||
1 | .upload-video-container { | ||
2 | border-radius: 3px; | ||
3 | background-color: #F7F7F7; | ||
4 | border: 3px solid #EAEAEA; | ||
5 | width: 100%; | ||
6 | height: 440px; | ||
7 | text-align: center; | ||
8 | margin-top: 40px; | ||
9 | display: flex; | ||
10 | justify-content: center; | ||
11 | align-items: center; | ||
12 | |||
13 | .upload-video { | ||
14 | display: flex; | ||
15 | flex-direction: column; | ||
16 | align-items: center; | ||
17 | |||
18 | .icon.icon-upload { | ||
19 | @include icon(90px); | ||
20 | margin-bottom: 25px; | ||
21 | cursor: default; | ||
22 | |||
23 | background-image: url('../../../assets/images/video/upload.svg'); | ||
24 | } | ||
25 | |||
26 | .button-file { | ||
27 | position: relative; | ||
28 | overflow: hidden; | ||
29 | display: inline-block; | ||
30 | margin-bottom: 70px; | ||
31 | |||
32 | @include peertube-button; | ||
33 | @include orange-button; | ||
34 | |||
35 | input[type=file] { | ||
36 | position: absolute; | ||
37 | top: 0; | ||
38 | right: 0; | ||
39 | min-width: 100%; | ||
40 | min-height: 100%; | ||
41 | font-size: 100px; | ||
42 | text-align: right; | ||
43 | filter: alpha(opacity=0); | ||
44 | opacity: 0; | ||
45 | outline: none; | ||
46 | background: white; | ||
47 | cursor: inherit; | ||
48 | display: block; | ||
49 | } | ||
50 | } | ||
51 | |||
52 | select { | ||
53 | @include peertube-select(auto); | ||
54 | |||
55 | display: inline-block; | ||
56 | font-size: 15px | ||
57 | } | ||
58 | } | ||
59 | } | ||
60 | |||
61 | p-progressBar { | ||
62 | /deep/ .ui-progressbar { | ||
63 | margin-top: 25px !important; | ||
64 | margin-bottom: 40px !important; | ||
65 | font-size: 15px !important; | ||
66 | color: #fff !important; | ||
67 | height: 30px !important; | ||
68 | line-height: 30px !important; | ||
69 | border-radius: 3px !important; | ||
70 | background-color: rgba(11, 204, 41, 0.16) !important; | ||
71 | |||
72 | .ui-progressbar-value { | ||
73 | background-color: #0BCC29 !important; | ||
74 | } | ||
75 | |||
76 | .ui-progressbar-label { | ||
77 | text-align: left; | ||
78 | padding-left: 18px; | ||
79 | margin-top: 0 !important; | ||
80 | } | ||
81 | } | ||
82 | |||
83 | &.processing { | ||
84 | /deep/ .ui-progressbar-label { | ||
85 | // Same color as background to hide "100%" | ||
86 | color: rgba(11, 204, 41, 0.16) !important; | ||
87 | |||
88 | &::before { | ||
89 | content: 'Processing...'; | ||
90 | color: #fff; | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | } | ||
95 | |||
96 | |||
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 1704cf486..2bbc3de17 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts | |||
@@ -1,68 +1,42 @@ | |||
1 | import { HttpEventType, HttpResponse } from '@angular/common/http' | ||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { FormBuilder, FormGroup } from '@angular/forms' | 3 | import { FormBuilder, FormGroup } from '@angular/forms' |
3 | import { Router } from '@angular/router' | 4 | import { Router } from '@angular/router' |
4 | |||
5 | import { NotificationsService } from 'angular2-notifications' | 5 | import { NotificationsService } from 'angular2-notifications' |
6 | 6 | import { VideoService } from 'app/shared/video/video.service' | |
7 | import { | ||
8 | FormReactive, | ||
9 | VIDEO_NAME, | ||
10 | VIDEO_CATEGORY, | ||
11 | VIDEO_LICENCE, | ||
12 | VIDEO_LANGUAGE, | ||
13 | VIDEO_DESCRIPTION, | ||
14 | VIDEO_TAGS, | ||
15 | VIDEO_CHANNEL, | ||
16 | VIDEO_FILE, | ||
17 | VIDEO_PRIVACY | ||
18 | } from '../../shared' | ||
19 | import { AuthService, ServerService } from '../../core' | ||
20 | import { VideoService } from '../shared' | ||
21 | import { VideoCreate } from '../../../../../shared' | 7 | import { VideoCreate } from '../../../../../shared' |
22 | import { HttpEventType, HttpResponse } from '@angular/common/http' | 8 | import { VideoPrivacy } from '../../../../../shared/models/videos' |
9 | import { AuthService, ServerService } from '../../core' | ||
10 | import { FormReactive } from '../../shared' | ||
11 | import { ValidatorMessage } from '../../shared/forms/form-validators' | ||
12 | import { VideoEdit } from '../../shared/video/video-edit.model' | ||
23 | 13 | ||
24 | @Component({ | 14 | @Component({ |
25 | selector: 'my-videos-add', | 15 | selector: 'my-videos-add', |
26 | styleUrls: [ './shared/video-edit.component.scss' ], | 16 | templateUrl: './video-add.component.html', |
27 | templateUrl: './video-add.component.html' | 17 | styleUrls: [ |
18 | './shared/video-edit.component.scss', | ||
19 | './video-add.component.scss' | ||
20 | ] | ||
28 | }) | 21 | }) |
29 | 22 | ||
30 | export class VideoAddComponent extends FormReactive implements OnInit { | 23 | export class VideoAddComponent extends FormReactive implements OnInit { |
31 | @ViewChild('videofileInput') videofileInput | 24 | @ViewChild('videofileInput') videofileInput |
32 | 25 | ||
33 | progressPercent = 0 | 26 | isUploadingVideo = false |
34 | tags: string[] = [] | 27 | videoUploaded = false |
35 | videoCategories = [] | 28 | videoUploadPercents = 0 |
36 | videoLicences = [] | 29 | videoUploadedId = 0 |
37 | videoLanguages = [] | ||
38 | videoPrivacies = [] | ||
39 | userVideoChannels = [] | ||
40 | |||
41 | tagValidators = VIDEO_TAGS.VALIDATORS | ||
42 | tagValidatorsMessages = VIDEO_TAGS.MESSAGES | ||
43 | 30 | ||
44 | error: string | 31 | error: string = null |
45 | form: FormGroup | 32 | form: FormGroup |
46 | formErrors = { | 33 | formErrors: { [ id: string ]: string } = {} |
47 | name: '', | 34 | validationMessages: ValidatorMessage = {} |
48 | privacy: '', | 35 | |
49 | category: '', | 36 | userVideoChannels = [] |
50 | licence: '', | 37 | videoPrivacies = [] |
51 | language: '', | 38 | firstStepPrivacyId = 0 |
52 | channelId: '', | 39 | firstStepChannelId = 0 |
53 | description: '', | ||
54 | videofile: '' | ||
55 | } | ||
56 | validationMessages = { | ||
57 | name: VIDEO_NAME.MESSAGES, | ||
58 | privacy: VIDEO_PRIVACY.MESSAGES, | ||
59 | category: VIDEO_CATEGORY.MESSAGES, | ||
60 | licence: VIDEO_LICENCE.MESSAGES, | ||
61 | language: VIDEO_LANGUAGE.MESSAGES, | ||
62 | channelId: VIDEO_CHANNEL.MESSAGES, | ||
63 | description: VIDEO_DESCRIPTION.MESSAGES, | ||
64 | videofile: VIDEO_FILE.MESSAGES | ||
65 | } | ||
66 | 40 | ||
67 | constructor ( | 41 | constructor ( |
68 | private formBuilder: FormBuilder, | 42 | private formBuilder: FormBuilder, |
@@ -75,35 +49,23 @@ export class VideoAddComponent extends FormReactive implements OnInit { | |||
75 | super() | 49 | super() |
76 | } | 50 | } |
77 | 51 | ||
78 | get filename () { | ||
79 | return this.form.value['videofile'] | ||
80 | } | ||
81 | |||
82 | buildForm () { | 52 | buildForm () { |
83 | this.form = this.formBuilder.group({ | 53 | this.form = this.formBuilder.group({}) |
84 | name: [ '', VIDEO_NAME.VALIDATORS ], | ||
85 | nsfw: [ false ], | ||
86 | privacy: [ '', VIDEO_PRIVACY.VALIDATORS ], | ||
87 | category: [ '', VIDEO_CATEGORY.VALIDATORS ], | ||
88 | licence: [ '', VIDEO_LICENCE.VALIDATORS ], | ||
89 | language: [ '', VIDEO_LANGUAGE.VALIDATORS ], | ||
90 | channelId: [ '', VIDEO_CHANNEL.VALIDATORS ], | ||
91 | description: [ '', VIDEO_DESCRIPTION.VALIDATORS ], | ||
92 | videofile: [ '', VIDEO_FILE.VALIDATORS ], | ||
93 | tags: [ '' ] | ||
94 | }) | ||
95 | |||
96 | this.form.valueChanges.subscribe(data => this.onValueChanged(data)) | 54 | this.form.valueChanges.subscribe(data => this.onValueChanged(data)) |
97 | } | 55 | } |
98 | 56 | ||
99 | ngOnInit () { | 57 | ngOnInit () { |
100 | this.videoCategories = this.serverService.getVideoCategories() | ||
101 | this.videoLicences = this.serverService.getVideoLicences() | ||
102 | this.videoLanguages = this.serverService.getVideoLanguages() | ||
103 | this.videoPrivacies = this.serverService.getVideoPrivacies() | ||
104 | |||
105 | this.buildForm() | 58 | this.buildForm() |
106 | 59 | ||
60 | this.serverService.videoPrivaciesLoaded | ||
61 | .subscribe( | ||
62 | () => { | ||
63 | this.videoPrivacies = this.serverService.getVideoPrivacies() | ||
64 | |||
65 | // Public by default | ||
66 | this.firstStepPrivacyId = VideoPrivacy.PUBLIC | ||
67 | }) | ||
68 | |||
107 | this.authService.userInformationLoaded | 69 | this.authService.userInformationLoaded |
108 | .subscribe( | 70 | .subscribe( |
109 | () => { | 71 | () => { |
@@ -114,21 +76,13 @@ export class VideoAddComponent extends FormReactive implements OnInit { | |||
114 | if (Array.isArray(videoChannels) === false) return | 76 | if (Array.isArray(videoChannels) === false) return |
115 | 77 | ||
116 | this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name })) | 78 | this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name })) |
117 | 79 | this.firstStepChannelId = this.userVideoChannels[0].id | |
118 | this.form.patchValue({ channelId: this.userVideoChannels[0].id }) | ||
119 | } | 80 | } |
120 | ) | 81 | ) |
121 | } | 82 | } |
122 | 83 | ||
123 | // The goal is to keep reactive form validation (required field) | 84 | fileChange () { |
124 | // https://stackoverflow.com/a/44238894 | 85 | this.uploadFirstStep() |
125 | fileChange ($event) { | ||
126 | this.form.controls['videofile'].setValue($event.target.files[0].name) | ||
127 | } | ||
128 | |||
129 | removeFile () { | ||
130 | this.videofileInput.nativeElement.value = '' | ||
131 | this.form.controls['videofile'].setValue('') | ||
132 | } | 86 | } |
133 | 87 | ||
134 | checkForm () { | 88 | checkForm () { |
@@ -137,62 +91,72 @@ export class VideoAddComponent extends FormReactive implements OnInit { | |||
137 | return this.form.valid | 91 | return this.form.valid |
138 | } | 92 | } |
139 | 93 | ||
140 | upload () { | 94 | uploadFirstStep () { |
141 | if (this.checkForm() === false) { | ||
142 | return | ||
143 | } | ||
144 | |||
145 | const formValue: VideoCreate = this.form.value | ||
146 | |||
147 | const name = formValue.name | ||
148 | const privacy = formValue.privacy | ||
149 | const nsfw = formValue.nsfw | ||
150 | const category = formValue.category | ||
151 | const licence = formValue.licence | ||
152 | const language = formValue.language | ||
153 | const channelId = formValue.channelId | ||
154 | const description = formValue.description | ||
155 | const tags = formValue.tags | ||
156 | const videofile = this.videofileInput.nativeElement.files[0] | 95 | const videofile = this.videofileInput.nativeElement.files[0] |
96 | const name = videofile.name.replace(/\.[^/.]+$/, '') | ||
97 | const privacy = this.firstStepPrivacyId.toString() | ||
98 | const nsfw = false | ||
99 | const channelId = this.firstStepChannelId.toString() | ||
157 | 100 | ||
158 | const formData = new FormData() | 101 | const formData = new FormData() |
159 | formData.append('name', name) | 102 | formData.append('name', name) |
160 | formData.append('privacy', privacy.toString()) | 103 | // Put the video "private" -> we wait he validates the second step |
161 | formData.append('category', '' + category) | 104 | formData.append('privacy', VideoPrivacy.PRIVATE.toString()) |
162 | formData.append('nsfw', '' + nsfw) | 105 | formData.append('nsfw', '' + nsfw) |
163 | formData.append('licence', '' + licence) | ||
164 | formData.append('channelId', '' + channelId) | 106 | formData.append('channelId', '' + channelId) |
165 | formData.append('videofile', videofile) | 107 | formData.append('videofile', videofile) |
166 | 108 | ||
167 | // Language is optional | 109 | this.isUploadingVideo = true |
168 | if (language) { | 110 | this.form.patchValue({ |
169 | formData.append('language', '' + language) | 111 | name, |
170 | } | 112 | privacy, |
171 | 113 | nsfw, | |
172 | formData.append('description', description) | 114 | channelId |
173 | 115 | }) | |
174 | for (let i = 0; i < tags.length; i++) { | ||
175 | formData.append(`tags[${i}]`, tags[i]) | ||
176 | } | ||
177 | 116 | ||
178 | this.videoService.uploadVideo(formData).subscribe( | 117 | this.videoService.uploadVideo(formData).subscribe( |
179 | event => { | 118 | event => { |
180 | if (event.type === HttpEventType.UploadProgress) { | 119 | if (event.type === HttpEventType.UploadProgress) { |
181 | this.progressPercent = Math.round(100 * event.loaded / event.total) | 120 | this.videoUploadPercents = Math.round(100 * event.loaded / event.total) |
182 | } else if (event instanceof HttpResponse) { | 121 | } else if (event instanceof HttpResponse) { |
183 | console.log('Video uploaded.') | 122 | console.log('Video uploaded.') |
184 | this.notificationsService.success('Success', 'Video uploaded.') | ||
185 | 123 | ||
186 | // Display all the videos once it's finished | 124 | this.videoUploaded = true |
187 | this.router.navigate([ '/videos/list' ]) | 125 | |
126 | this.videoUploadedId = event.body.video.id | ||
188 | } | 127 | } |
189 | }, | 128 | }, |
190 | 129 | ||
191 | err => { | 130 | err => { |
192 | // Reset progress | 131 | // Reset progress |
193 | this.progressPercent = 0 | 132 | this.videoUploadPercents = 0 |
194 | this.error = err.message | 133 | this.error = err.message |
195 | } | 134 | } |
196 | ) | 135 | ) |
197 | } | 136 | } |
137 | |||
138 | updateSecondStep () { | ||
139 | if (this.checkForm() === false) { | ||
140 | return | ||
141 | } | ||
142 | |||
143 | const video = new VideoEdit() | ||
144 | video.patch(this.form.value) | ||
145 | video.channel = this.firstStepChannelId | ||
146 | video.id = this.videoUploadedId | ||
147 | |||
148 | this.videoService.updateVideo(video) | ||
149 | .subscribe( | ||
150 | () => { | ||
151 | this.notificationsService.success('Success', 'Video published.') | ||
152 | this.router.navigate([ '/videos/watch', video.id ]) | ||
153 | }, | ||
154 | |||
155 | err => { | ||
156 | this.error = 'Cannot update the video.' | ||
157 | console.error(err) | ||
158 | } | ||
159 | ) | ||
160 | |||
161 | } | ||
198 | } | 162 | } |
diff --git a/client/src/app/videos/+video-edit/video-add.module.ts b/client/src/app/videos/+video-edit/video-add.module.ts index f58d12dac..1efecdf4d 100644 --- a/client/src/app/videos/+video-edit/video-add.module.ts +++ b/client/src/app/videos/+video-edit/video-add.module.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { ProgressBarModule } from 'primeng/primeng' | ||
2 | import { SharedModule } from '../../shared' | 3 | import { SharedModule } from '../../shared' |
3 | import { VideoEditModule } from './shared/video-edit.module' | 4 | import { VideoEditModule } from './shared/video-edit.module' |
4 | import { VideoAddRoutingModule } from './video-add-routing.module' | 5 | import { VideoAddRoutingModule } from './video-add-routing.module' |
@@ -8,7 +9,8 @@ import { VideoAddComponent } from './video-add.component' | |||
8 | imports: [ | 9 | imports: [ |
9 | VideoAddRoutingModule, | 10 | VideoAddRoutingModule, |
10 | VideoEditModule, | 11 | VideoEditModule, |
11 | SharedModule | 12 | SharedModule, |
13 | ProgressBarModule | ||
12 | ], | 14 | ], |
13 | 15 | ||
14 | declarations: [ | 16 | declarations: [ |
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html index b9c6139b2..261b8a130 100644 --- a/client/src/app/videos/+video-edit/video-update.component.html +++ b/client/src/app/videos/+video-edit/video-update.component.html | |||
@@ -1,101 +1,20 @@ | |||
1 | <div class="row"> | 1 | <div class="margin-content"> |
2 | <div class="content-padding"> | 2 | <div class="title-page title-page-single"> |
3 | 3 | Update {{ video?.name }} | |
4 | <h3>Update {{ video?.name }}</h3> | 4 | </div> |
5 | |||
6 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
7 | 5 | ||
8 | <form novalidate [formGroup]="form"> | 6 | <form novalidate [formGroup]="form"> |
9 | <div class="form-group"> | ||
10 | <label for="name">Name</label> | ||
11 | <input | ||
12 | type="text" class="form-control" id="name" | ||
13 | formControlName="name" | ||
14 | > | ||
15 | <div *ngIf="formErrors.name" class="alert alert-danger"> | ||
16 | {{ formErrors.name }} | ||
17 | </div> | ||
18 | </div> | ||
19 | |||
20 | <div class="form-group"> | ||
21 | <label for="privacy">Privacy</label> | ||
22 | <select class="form-control" id="privacy" formControlName="privacy"> | ||
23 | <option></option> | ||
24 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | ||
25 | </select> | ||
26 | |||
27 | <div *ngIf="formErrors.privacy" class="alert alert-danger"> | ||
28 | {{ formErrors.privacy }} | ||
29 | </div> | ||
30 | </div> | ||
31 | |||
32 | <div class="form-group"> | ||
33 | <input | ||
34 | type="checkbox" id="nsfw" | ||
35 | formControlName="nsfw" | ||
36 | > | ||
37 | <label for="nsfw">This video contains mature or explicit content</label> | ||
38 | </div> | ||
39 | |||
40 | <div class="form-group"> | ||
41 | <label for="category">Category</label> | ||
42 | <select class="form-control" id="category" formControlName="category"> | ||
43 | <option></option> | ||
44 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> | ||
45 | </select> | ||
46 | |||
47 | <div *ngIf="formErrors.category" class="alert alert-danger"> | ||
48 | {{ formErrors.category }} | ||
49 | </div> | ||
50 | </div> | ||
51 | |||
52 | <div class="form-group"> | ||
53 | <label for="licence">Licence</label> | ||
54 | <select class="form-control" id="licence" formControlName="licence"> | ||
55 | <option></option> | ||
56 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> | ||
57 | </select> | ||
58 | |||
59 | <div *ngIf="formErrors.licence" class="alert alert-danger"> | ||
60 | {{ formErrors.licence }} | ||
61 | </div> | ||
62 | </div> | ||
63 | |||
64 | <div class="form-group"> | ||
65 | <label for="language">Language</label> | ||
66 | <select class="form-control" id="language" formControlName="language"> | ||
67 | <option></option> | ||
68 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> | ||
69 | </select> | ||
70 | |||
71 | <div *ngIf="formErrors.language" class="alert alert-danger"> | ||
72 | {{ formErrors.language }} | ||
73 | </div> | ||
74 | </div> | ||
75 | |||
76 | <div class="form-group"> | ||
77 | <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span> | ||
78 | <tag-input | ||
79 | [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | ||
80 | formControlName="tags" maxItems="5" modelAsStrings="true" | ||
81 | ></tag-input> | ||
82 | </div> | ||
83 | 7 | ||
84 | <div class="form-group"> | 8 | <my-video-edit |
85 | <label for="description">Description</label> | 9 | [form]="form" [formErrors]="formErrors" |
86 | <my-video-description formControlName="description"></my-video-description> | 10 | [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" |
11 | ></my-video-edit> | ||
87 | 12 | ||
88 | <div *ngIf="formErrors.description" class="alert alert-danger"> | 13 | <div class="submit-container"> |
89 | {{ formErrors.description }} | 14 | <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid }"> |
15 | <span class="icon icon-validate"></span> | ||
16 | <input type="button" value="Update" /> | ||
90 | </div> | 17 | </div> |
91 | </div> | 18 | </div> |
92 | |||
93 | <div class="form-group"> | ||
94 | <input | ||
95 | type="button" value="Update" class="btn btn-default form-control" | ||
96 | (click)="update()" | ||
97 | > | ||
98 | </div> | ||
99 | </form> | 19 | </form> |
100 | </div> | ||
101 | </div> | 20 | </div> |
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 0e966cb50..d1da8b6d8 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts | |||
@@ -1,23 +1,14 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { FormBuilder, FormGroup } from '@angular/forms' | 2 | import { FormBuilder, FormGroup } from '@angular/forms' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import 'rxjs/add/observable/forkJoin' | ||
5 | |||
6 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
7 | 5 | import 'rxjs/add/observable/forkJoin' | |
8 | import { ServerService } from '../../core' | ||
9 | import { | ||
10 | FormReactive, | ||
11 | VIDEO_NAME, | ||
12 | VIDEO_CATEGORY, | ||
13 | VIDEO_LICENCE, | ||
14 | VIDEO_LANGUAGE, | ||
15 | VIDEO_DESCRIPTION, | ||
16 | VIDEO_TAGS, | ||
17 | VIDEO_PRIVACY | ||
18 | } from '../../shared' | ||
19 | import { VideoEdit, VideoService } from '../shared' | ||
20 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' | 6 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' |
7 | import { ServerService } from '../../core' | ||
8 | import { FormReactive } from '../../shared' | ||
9 | import { ValidatorMessage } from '../../shared/forms/form-validators' | ||
10 | import { VideoEdit } from '../../shared/video/video-edit.model' | ||
11 | import { VideoService } from '../../shared/video/video.service' | ||
21 | 12 | ||
22 | @Component({ | 13 | @Component({ |
23 | selector: 'my-videos-update', | 14 | selector: 'my-videos-update', |
@@ -26,34 +17,13 @@ import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy. | |||
26 | }) | 17 | }) |
27 | 18 | ||
28 | export class VideoUpdateComponent extends FormReactive implements OnInit { | 19 | export class VideoUpdateComponent extends FormReactive implements OnInit { |
29 | tags: string[] = [] | ||
30 | videoCategories = [] | ||
31 | videoLicences = [] | ||
32 | videoLanguages = [] | ||
33 | videoPrivacies = [] | ||
34 | video: VideoEdit | 20 | video: VideoEdit |
35 | 21 | ||
36 | tagValidators = VIDEO_TAGS.VALIDATORS | ||
37 | tagValidatorsMessages = VIDEO_TAGS.MESSAGES | ||
38 | |||
39 | error: string = null | 22 | error: string = null |
40 | form: FormGroup | 23 | form: FormGroup |
41 | formErrors = { | 24 | formErrors: { [ id: string ]: string } = {} |
42 | name: '', | 25 | validationMessages: ValidatorMessage = {} |
43 | privacy: '', | 26 | videoPrivacies = [] |
44 | category: '', | ||
45 | licence: '', | ||
46 | language: '', | ||
47 | description: '' | ||
48 | } | ||
49 | validationMessages = { | ||
50 | name: VIDEO_NAME.MESSAGES, | ||
51 | privacy: VIDEO_PRIVACY.MESSAGES, | ||
52 | category: VIDEO_CATEGORY.MESSAGES, | ||
53 | licence: VIDEO_LICENCE.MESSAGES, | ||
54 | language: VIDEO_LANGUAGE.MESSAGES, | ||
55 | description: VIDEO_DESCRIPTION.MESSAGES | ||
56 | } | ||
57 | 27 | ||
58 | fileError = '' | 28 | fileError = '' |
59 | 29 | ||
@@ -69,30 +39,16 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
69 | } | 39 | } |
70 | 40 | ||
71 | buildForm () { | 41 | buildForm () { |
72 | this.form = this.formBuilder.group({ | 42 | this.form = this.formBuilder.group({}) |
73 | name: [ '', VIDEO_NAME.VALIDATORS ], | ||
74 | privacy: [ '', VIDEO_PRIVACY.VALIDATORS ], | ||
75 | nsfw: [ false ], | ||
76 | category: [ '', VIDEO_CATEGORY.VALIDATORS ], | ||
77 | licence: [ '', VIDEO_LICENCE.VALIDATORS ], | ||
78 | language: [ '', VIDEO_LANGUAGE.VALIDATORS ], | ||
79 | description: [ '', VIDEO_DESCRIPTION.VALIDATORS ], | ||
80 | tags: [ '' ] | ||
81 | }) | ||
82 | |||
83 | this.form.valueChanges.subscribe(data => this.onValueChanged(data)) | 43 | this.form.valueChanges.subscribe(data => this.onValueChanged(data)) |
84 | } | 44 | } |
85 | 45 | ||
86 | ngOnInit () { | 46 | ngOnInit () { |
87 | this.buildForm() | 47 | this.buildForm() |
88 | 48 | ||
89 | this.videoCategories = this.serverService.getVideoCategories() | ||
90 | this.videoLicences = this.serverService.getVideoLicences() | ||
91 | this.videoLanguages = this.serverService.getVideoLanguages() | ||
92 | this.videoPrivacies = this.serverService.getVideoPrivacies() | 49 | this.videoPrivacies = this.serverService.getVideoPrivacies() |
93 | 50 | ||
94 | const uuid: string = this.route.snapshot.params['uuid'] | 51 | const uuid: string = this.route.snapshot.params['uuid'] |
95 | |||
96 | this.videoService.getVideo(uuid) | 52 | this.videoService.getVideo(uuid) |
97 | .switchMap(video => { | 53 | .switchMap(video => { |
98 | return this.videoService | 54 | return this.videoService |
@@ -104,7 +60,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
104 | video => { | 60 | video => { |
105 | this.video = new VideoEdit(video) | 61 | this.video = new VideoEdit(video) |
106 | 62 | ||
107 | // We cannot set private a video that was not private anymore | 63 | // We cannot set private a video that was not private |
108 | if (video.privacy !== VideoPrivacy.PRIVATE) { | 64 | if (video.privacy !== VideoPrivacy.PRIVATE) { |
109 | const newVideoPrivacies = [] | 65 | const newVideoPrivacies = [] |
110 | for (const p of this.videoPrivacies) { | 66 | for (const p of this.videoPrivacies) { |
diff --git a/client/src/app/videos/+video-watch/video-download.component.html b/client/src/app/videos/+video-watch/video-download.component.html index ddc57e999..7efc79e93 100644 --- a/client/src/app/videos/+video-watch/video-download.component.html +++ b/client/src/app/videos/+video-watch/video-download.component.html | |||
@@ -6,18 +6,19 @@ | |||
6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> | 6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> |
7 | <span aria-hidden="true">×</span> | 7 | <span aria-hidden="true">×</span> |
8 | </button> | 8 | </button> |
9 | <h4 class="modal-title">Download</h4> | 9 | <h4 class="title-page title-page-single">Download</h4> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | <div class="modal-body"> | 12 | <div class="modal-body"> |
13 | <div *ngFor="let file of video.files" class="resolution-block"> | 13 | <div *ngFor="let file of video.files" class="resolution-block"> |
14 | <label>{{ file.resolutionLabel }}</label> | 14 | <label>{{ file.resolutionLabel }}</label> |
15 | <a class="btn btn-default " target="_blank" [href]="file.torrentUrl"> | 15 | |
16 | <span class="glyphicon glyphicon-download"></span> | 16 | <a class="orange-button-link " target="_blank" [href]="file.torrentUrl"> |
17 | <span class="icon icon-download"></span> | ||
17 | Torrent file | 18 | Torrent file |
18 | </a> | 19 | </a> |
19 | <a class="btn btn-default" target="_blank" [href]="file.fileUrl"> | 20 | <a class="orange-button-link" target="_blank" [href]="file.fileUrl"> |
20 | <span class="glyphicon glyphicon-download"></span> | 21 | <span class="icon icon-download"></span> |
21 | Download | 22 | Download |
22 | </a> | 23 | </a> |
23 | 24 | ||
diff --git a/client/src/app/videos/+video-watch/video-download.component.scss b/client/src/app/videos/+video-watch/video-download.component.scss new file mode 100644 index 000000000..c9d5af9c1 --- /dev/null +++ b/client/src/app/videos/+video-watch/video-download.component.scss | |||
@@ -0,0 +1,23 @@ | |||
1 | .resolution-block:not(:first-child) { | ||
2 | margin-top: 30px; | ||
3 | } | ||
4 | |||
5 | .orange-button-link { | ||
6 | margin-right: 10px; | ||
7 | } | ||
8 | |||
9 | label { | ||
10 | display: block; | ||
11 | } | ||
12 | |||
13 | .icon { | ||
14 | @include icon(21px); | ||
15 | |||
16 | margin-right: 5px; | ||
17 | position: relative; | ||
18 | top: -1px; | ||
19 | |||
20 | &.icon-download { | ||
21 | background-image: url('../../../assets/images/video/download-white.svg'); | ||
22 | } | ||
23 | } | ||
diff --git a/client/src/app/videos/+video-watch/video-download.component.ts b/client/src/app/videos/+video-watch/video-download.component.ts index c32f8d586..095df1698 100644 --- a/client/src/app/videos/+video-watch/video-download.component.ts +++ b/client/src/app/videos/+video-watch/video-download.component.ts | |||
@@ -1,13 +1,11 @@ | |||
1 | import { Component, Input, ViewChild } from '@angular/core' | 1 | import { Component, Input, ViewChild } from '@angular/core' |
2 | |||
3 | import { ModalDirective } from 'ngx-bootstrap/modal' | 2 | import { ModalDirective } from 'ngx-bootstrap/modal' |
4 | 3 | import { VideoDetails } from '../../shared/video/video-details.model' | |
5 | import { VideoDetails } from '../shared' | ||
6 | 4 | ||
7 | @Component({ | 5 | @Component({ |
8 | selector: 'my-video-download', | 6 | selector: 'my-video-download', |
9 | templateUrl: './video-download.component.html', | 7 | templateUrl: './video-download.component.html', |
10 | styles: [ '.resolution-block { margin-top: 20px; }' ] | 8 | styleUrls: [ './video-download.component.scss' ] |
11 | }) | 9 | }) |
12 | export class VideoDownloadComponent { | 10 | export class VideoDownloadComponent { |
13 | @Input() video: VideoDetails = null | 11 | @Input() video: VideoDetails = null |
diff --git a/client/src/app/videos/+video-watch/video-report.component.html b/client/src/app/videos/+video-watch/video-report.component.html index ceb7cf50a..20474bab4 100644 --- a/client/src/app/videos/+video-watch/video-report.component.html +++ b/client/src/app/videos/+video-watch/video-report.component.html | |||
@@ -6,28 +6,28 @@ | |||
6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> | 6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> |
7 | <span aria-hidden="true">×</span> | 7 | <span aria-hidden="true">×</span> |
8 | </button> | 8 | </button> |
9 | <h4 class="modal-title">Report video</h4> | 9 | <h4 class="title-page title-page-single">Report video</h4> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | <div class="modal-body"> | 12 | <div class="modal-body"> |
13 | 13 | ||
14 | <form novalidate [formGroup]="form"> | 14 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> |
15 | <div class="form-group"> | 15 | <div class="form-group"> |
16 | <label for="reason">Reason</label> | 16 | <label for="reason">Reason</label> |
17 | <textarea | 17 | <textarea |
18 | id="reason" class="form-control" placeholder="Reason..." | 18 | id="reason" class="form-control" placeholder="Reason..." |
19 | formControlName="reason" | 19 | formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }" |
20 | > | 20 | > |
21 | </textarea> | 21 | </textarea> |
22 | <div *ngIf="formErrors.reason" class="alert alert-danger"> | 22 | <div *ngIf="formErrors.reason" class="form-error"> |
23 | {{ formErrors.reason }} | 23 | {{ formErrors.reason }} |
24 | </div> | 24 | </div> |
25 | </div> | 25 | </div> |
26 | 26 | ||
27 | <div class="form-group"> | 27 | <div class="form-group"> |
28 | <input | 28 | <input |
29 | type="button" value="Report" class="btn btn-default form-control" | 29 | type="submit" value="Report" class="orange-button" |
30 | [disabled]="!form.valid" (click)="report()" | 30 | [disabled]="!form.valid" |
31 | > | 31 | > |
32 | </div> | 32 | </div> |
33 | </form> | 33 | </form> |
diff --git a/client/src/app/videos/+video-watch/video-report.component.ts b/client/src/app/videos/+video-watch/video-report.component.ts index fc9b5a9d4..b94e4144e 100644 --- a/client/src/app/videos/+video-watch/video-report.component.ts +++ b/client/src/app/videos/+video-watch/video-report.component.ts | |||
@@ -1,11 +1,9 @@ | |||
1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
2 | import { FormBuilder, FormGroup } from '@angular/forms' | 2 | import { FormBuilder, FormGroup } from '@angular/forms' |
3 | |||
4 | import { ModalDirective } from 'ngx-bootstrap/modal' | ||
5 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
6 | 4 | import { ModalDirective } from 'ngx-bootstrap/modal' | |
7 | import { FormReactive, VideoAbuseService, VIDEO_ABUSE_REASON } from '../../shared' | 5 | import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../shared' |
8 | import { VideoDetails, VideoService } from '../shared' | 6 | import { VideoDetails } from '../../shared/video/video-details.model' |
9 | 7 | ||
10 | @Component({ | 8 | @Component({ |
11 | selector: 'my-video-report', | 9 | selector: 'my-video-report', |
diff --git a/client/src/app/videos/+video-watch/video-share.component.html b/client/src/app/videos/+video-watch/video-share.component.html index 88f59c063..36ec38d88 100644 --- a/client/src/app/videos/+video-watch/video-share.component.html +++ b/client/src/app/videos/+video-watch/video-share.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> | 6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> |
7 | <span aria-hidden="true">×</span> | 7 | <span aria-hidden="true">×</span> |
8 | </button> | 8 | </button> |
9 | <h4 class="modal-title">Share</h4> | 9 | <h4 class="title-page title-page-single">Share</h4> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | <div class="modal-body"> | 12 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/video-share.component.ts b/client/src/app/videos/+video-watch/video-share.component.ts index aeef65ecf..4df9adf29 100644 --- a/client/src/app/videos/+video-watch/video-share.component.ts +++ b/client/src/app/videos/+video-watch/video-share.component.ts | |||
@@ -1,8 +1,6 @@ | |||
1 | import { Component, Input, ViewChild } from '@angular/core' | 1 | import { Component, Input, ViewChild } from '@angular/core' |
2 | |||
3 | import { ModalDirective } from 'ngx-bootstrap/modal' | 2 | import { ModalDirective } from 'ngx-bootstrap/modal' |
4 | 3 | import { VideoDetails } from '../../shared/video/video-details.model' | |
5 | import { VideoDetails } from '../shared' | ||
6 | 4 | ||
7 | @Component({ | 5 | @Component({ |
8 | selector: 'my-video-share', | 6 | selector: 'my-video-share', |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index f528d73c3..f99e84caf 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html | |||
@@ -1,197 +1,153 @@ | |||
1 | <div *ngIf="error" class="row"> | ||
2 | <div class="alert alert-danger"> | ||
3 | The video load seems to be abnormally long. | ||
4 | <ul> | ||
5 | <li>Maybe the server {{ video.serverHost }} is down :(</li> | ||
6 | <li> | ||
7 | If not, you can report an issue on | ||
8 | <a href="https://github.com/Chocobozzz/PeerTube/issues" title="Report an issue"> | ||
9 | https://github.com/Chocobozzz/PeerTube/issues | ||
10 | </a> | ||
11 | </li> | ||
12 | </ul> | ||
13 | </div> | ||
14 | </div> | ||
15 | |||
16 | <div class="row"> | 1 | <div class="row"> |
17 | <!-- We need the video container for videojs so we just hide it --> | 2 | <!-- We need the video container for videojs so we just hide it --> |
18 | <div [hidden]="videoNotFound" class="embed-responsive embed-responsive-19by9"> | 3 | <div [hidden]="videoNotFound" id="video-container"> |
19 | <video id="video-container" class="video-js vjs-sublime-skin"></video> | 4 | <video id="video-element" class="video-js vjs-peertube-skin vjs-fluid"></video> |
20 | </div> | 5 | </div> |
21 | 6 | ||
22 | <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div> | 7 | <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div> |
23 | </div> | ||
24 | |||
25 | <!-- P2P information --> | ||
26 | <div id="torrent-info" class="row"> | ||
27 | <div id="torrent-info-download" class="col-md-4 col-sm-4 col-xs-4">Download: {{ downloadSpeed | bytes }}/s</div> | ||
28 | <div id="torrent-info-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div> | ||
29 | <div id="torrent-info-peers" class="col-md-4 col-sm-4 col-xs-4">Number of peers: {{ numPeers }}</div> | ||
30 | </div> | ||
31 | |||
32 | <!-- Video information --> | ||
33 | <div *ngIf="video !== null" id="video-info"> | ||
34 | <div class="row video-name-views"> | ||
35 | <div class="col-xs-8 col-md-8 video-name"> | ||
36 | {{ video.name }} | ||
37 | </div> | ||
38 | |||
39 | <div class="col-xs-4 col-md-4 pull-right video-views"> | ||
40 | {{ video.views}} views | ||
41 | </div> | ||
42 | </div> | ||
43 | |||
44 | <div class="row video-small-blocks"> | ||
45 | <div class="col-xs-5 col-xs-3 col-md-3 video-small-block video-small-block-account"> | ||
46 | <a class="option" title="Access to all videos of this user" [routerLink]="['/videos/list', { field: 'account', search: video.account }]"> | ||
47 | <span class="glyphicon glyphicon-user"></span> | ||
48 | <span class="video-small-block-text">{{ video.by }}</span> | ||
49 | </a> | ||
50 | </div> | ||
51 | |||
52 | <div class="col-xs-2 col-md-3 video-small-block video-small-block-share"> | ||
53 | <a class="option" (click)="showShareModal()" title="Share the video"> | ||
54 | <span class="glyphicon glyphicon-share"></span> | ||
55 | <span class="hidden-xs video-small-block-text">Share</span> | ||
56 | </a> | ||
57 | </div> | ||
58 | 8 | ||
59 | <div class="col-xs-2 col-md-3 video-small-block video-small-block-more"> | 9 | <!-- Video information --> |
60 | <div class="video-small-block-dropdown" dropdown dropup="true" placement="right"> | 10 | <div *ngIf="video" class="margin-content video-bottom"> |
61 | <a class="option" title="Access to more options" dropdownToggle> | 11 | <div class="video-info"> |
62 | <span class="glyphicon glyphicon-option-horizontal"></span> | 12 | <div class="video-info-name-actions"> |
63 | <span class="hidden-xs video-small-block-text">More</span> | 13 | <div class="video-info-name">{{ video.name }}</div> |
64 | </a> | 14 | |
65 | 15 | <div class="video-info-actions"> | |
66 | <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button"> | 16 | <div *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" class="action-button"> |
67 | <li *ngIf="canUserUpdateVideo()" role="menuitem"> | 17 | <span class="icon icon-like" title="Like this video" (click)="setLike()"></span> |
68 | <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/edit', video.uuid ]"> | 18 | </div> |
69 | <span class="glyphicon glyphicon-pencil"></span> Update | 19 | |
70 | </a> | 20 | <div *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" class="action-button"> |
71 | </li> | 21 | <span class="icon icon-dislike" title="Dislike this video" (click)="setDislike()"></span> |
72 | 22 | </div> | |
73 | <li role="menuitem"> | 23 | |
74 | <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)"> | 24 | <div (click)="showShareModal()" class="action-button"> |
75 | <span class="glyphicon glyphicon-download-alt"></span> Download | 25 | <span class="icon icon-share"></span> |
76 | </a> | 26 | Share |
77 | </li> | 27 | </div> |
78 | 28 | ||
79 | <li *ngIf="isUserLoggedIn()" role="menuitem"> | 29 | <div class="action-more" dropdown dropup="true" placement="right"> |
80 | <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)"> | 30 | <div class="action-button" dropdownToggle> |
81 | <span class="glyphicon glyphicon-alert"></span> Report | 31 | <span class="icon icon-more"></span> |
82 | </a> | 32 | </div> |
83 | </li> | 33 | |
84 | 34 | <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button"> | |
85 | <li *ngIf="isVideoRemovable()" role="menuitem"> | 35 | <li role="menuitem"> |
86 | <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)"> | 36 | <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)"> |
87 | <span class="glyphicon glyphicon-remove"></span> Delete | 37 | <span class="icon icon-download"></span> Download |
88 | </a> | 38 | </a> |
89 | </li> | 39 | </li> |
90 | 40 | ||
91 | <li *ngIf="isVideoBlacklistable()" role="menuitem"> | 41 | <li *ngIf="isUserLoggedIn()" role="menuitem"> |
92 | <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)"> | 42 | <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)"> |
93 | <span class="glyphicon glyphicon-eye-close"></span> Blacklist | 43 | <span class="icon icon-alert"></span> Report |
94 | </a> | 44 | </a> |
95 | </li> | 45 | </li> |
96 | </ul> | 46 | |
47 | <li *ngIf="isVideoBlacklistable()" role="menuitem"> | ||
48 | <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)"> | ||
49 | <span class="icon icon-blacklist"></span> Blacklist | ||
50 | </a> | ||
51 | </li> | ||
52 | </ul> | ||
53 | </div> | ||
54 | </div> | ||
97 | </div> | 55 | </div> |
98 | </div> | ||
99 | 56 | ||
100 | <div class="col-xs-3 col-md-3 video-small-block video-small-block-rating"> | 57 | <div class="video-info-date-views-bar"> |
101 | <div class="video-small-block-like"> | 58 | <div class="video-info-date-views"> |
102 | <span | 59 | {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views |
103 | class="glyphicon glyphicon-thumbs-up" title="Like this video" | 60 | </div> |
104 | [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'like' }" (click)="setLike()" | ||
105 | ></span> | ||
106 | 61 | ||
107 | <span class="video-small-block-text"> | 62 | <div *ngIf="video.likes !== 0 || video.dislikes !== 0" class="video-info-likes-dislikes-bar"> |
108 | {{ video.likes }} | 63 | <div class="likes-bar" [ngStyle]="{ 'width.%': video.likesPercent }"></div> |
109 | </span> | 64 | </div> |
110 | </div> | 65 | </div> |
111 | 66 | ||
112 | <div class="video-small-block-dislike"> | 67 | <div class="video-info-channel"> |
113 | <span | 68 | {{ video.channel.name }} |
114 | class="glyphicon glyphicon-thumbs-down" title="Dislike this video" | 69 | <!-- Here will be the subscribe button --> |
115 | [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'dislike' }" (click)="setDislike()" | ||
116 | ></span> | ||
117 | |||
118 | <span class="video-small-block-text"> | ||
119 | {{ video.dislikes }} | ||
120 | </span> | ||
121 | </div> | 70 | </div> |
122 | </div> | ||
123 | </div> | ||
124 | 71 | ||
125 | <div class="row video-details"> | 72 | <div class="video-info-by"> |
126 | <div class="video-details-date-description col-xs-8 col-md-9"> | 73 | By {{ video.by }} |
127 | <div class="video-details-date"> | 74 | <img [src]="getAvatarPath()" alt="Account avatar" /> |
128 | Published on {{ video.createdAt | date:'short' }} | ||
129 | </div> | 75 | </div> |
130 | 76 | ||
131 | <div class="video-details-description" [innerHTML]="videoHTMLDescription"></div> | 77 | <div class="video-info-description"> |
78 | <div class="video-info-description-html" [innerHTML]="videoHTMLDescription"></div> | ||
132 | 79 | ||
133 | <div class="video-details-description-more" *ngIf="completeDescriptionShown === false && video.description.length === 250" (click)="showMoreDescription()"> | 80 | <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length === 250" (click)="showMoreDescription()"> |
134 | Show more | 81 | Show more |
135 | <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span> | 82 | <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span> |
136 | <my-loader class="description-loading" [loading]="descriptionLoading"></my-loader> | 83 | <my-loader class="description-loading" [loading]="descriptionLoading"></my-loader> |
137 | </div> | 84 | </div> |
138 | 85 | ||
139 | <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-details-description-more"> | 86 | <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more"> |
140 | Show less | 87 | Show less |
141 | <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span> | 88 | <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span> |
89 | </div> | ||
142 | </div> | 90 | </div> |
143 | </div> | ||
144 | 91 | ||
145 | <div class="video-details-attributes col-xs-4 col-md-3"> | 92 | <div class="video-attributes"> |
146 | <div class="video-details-attribute"> | 93 | <div class="video-attribute"> |
147 | <span class="video-details-attribute-label"> | 94 | <span class="video-attribute-label"> |
148 | Privacy: | 95 | Privacy |
149 | </span> | 96 | </span> |
150 | <span class="video-details-attribute-value"> | 97 | <span class="video-attribute-value"> |
151 | {{ video.privacyLabel }} | 98 | {{ video.privacyLabel }} |
152 | </span> | 99 | </span> |
153 | </div> | 100 | </div> |
154 | 101 | ||
155 | <div class="video-details-attribute"> | 102 | <div class="video-attribute"> |
156 | <span class="video-details-attribute-label"> | 103 | <span class="video-attribute-label"> |
157 | Category: | 104 | Category |
158 | </span> | 105 | </span> |
159 | <span class="video-details-attribute-value"> | 106 | <span class="video-attribute-value"> |
160 | {{ video.categoryLabel }} | 107 | {{ video.categoryLabel }} |
161 | </span> | 108 | </span> |
162 | </div> | 109 | </div> |
163 | 110 | ||
164 | <div class="video-details-attribute"> | 111 | <div class="video-attribute"> |
165 | <span class="video-details-attribute-label"> | 112 | <span class="video-attribute-label"> |
166 | Licence: | 113 | Licence |
167 | </span> | 114 | </span> |
168 | <span class="video-details-attribute-value"> | 115 | <span class="video-attribute-value"> |
169 | {{ video.licenceLabel }} | 116 | {{ video.licenceLabel }} |
170 | </span> | 117 | </span> |
171 | </div> | 118 | </div> |
172 | 119 | ||
173 | <div class="video-details-attribute"> | 120 | <div class="video-attribute"> |
174 | <span class="video-details-attribute-label"> | 121 | <span class="video-attribute-label"> |
175 | Language: | 122 | Language |
176 | </span> | 123 | </span> |
177 | <span class="video-details-attribute-value"> | 124 | <span class="video-attribute-value"> |
178 | {{ video.languageLabel }} | 125 | {{ video.languageLabel }} |
179 | </span> | 126 | </span> |
180 | </div> | 127 | </div> |
181 | 128 | ||
182 | <div class="video-details-attribute"> | 129 | <div class="video-attribute"> |
183 | <span class="video-details-attribute-label"> | 130 | <span class="video-attribute-label"> |
184 | Tags: | 131 | Tags |
185 | </span> | 132 | </span> |
186 | 133 | ||
187 | <div class="video-details-tags"> | 134 | <span class="video-attribute-value"> |
188 | <a *ngFor="let tag of video.tags" [routerLink]="['/videos/list', { field: 'tags', search: tag }]" class="label label-primary"> | 135 | {{ getVideoTags() }} |
189 | {{ tag }} | 136 | </span> |
190 | </a> | ||
191 | </div> | 137 | </div> |
192 | </div> | 138 | </div> |
193 | 139 | ||
194 | </div> | 140 | </div> |
141 | |||
142 | <div class="other-videos"> | ||
143 | <div class="title-page title-page-single"> | ||
144 | Other videos | ||
145 | </div> | ||
146 | |||
147 | <div *ngFor="let video of otherVideos"> | ||
148 | <my-video-miniature [video]="video" [user]="user"></my-video-miniature> | ||
149 | </div> | ||
150 | </div> | ||
195 | </div> | 151 | </div> |
196 | </div> | 152 | </div> |
197 | 153 | ||
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss index cad21dd18..9daa757b4 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss | |||
@@ -1,6 +1,22 @@ | |||
1 | #video-container { | 1 | #video-container { |
2 | width: 100%; | 2 | background-color: #000; |
3 | height: 100%; | 3 | display: flex; |
4 | justify-content: center; | ||
5 | |||
6 | #video-element { | ||
7 | width: 888px; | ||
8 | height: 500px; | ||
9 | |||
10 | @media screen and (max-width: 800px) { | ||
11 | height: auto; | ||
12 | } | ||
13 | |||
14 | // VideoJS create an inner video player | ||
15 | video { | ||
16 | outline: 0; | ||
17 | position: relative !important; | ||
18 | } | ||
19 | } | ||
4 | } | 20 | } |
5 | 21 | ||
6 | #video-not-found { | 22 | #video-not-found { |
@@ -11,175 +27,153 @@ | |||
11 | font-weight: bold; | 27 | font-weight: bold; |
12 | } | 28 | } |
13 | 29 | ||
14 | .embed-responsive { | 30 | .video-bottom { |
15 | height: 500px; | 31 | margin-top: 40px; |
32 | display: flex; | ||
16 | 33 | ||
17 | @media screen and (max-width: 600px) { | 34 | .video-info { |
18 | height: 300px; | 35 | flex-grow: 1; |
19 | } | 36 | margin-right: 28px; |
20 | } | ||
21 | 37 | ||
22 | #torrent-info { | 38 | .video-info-name-actions { |
23 | font-size: 10px; | 39 | display: flex; |
24 | margin-top: 10px; | 40 | align-items: center; |
25 | text-align: center; | ||
26 | |||
27 | div { | ||
28 | min-width: 60px; | ||
29 | } | ||
30 | } | ||
31 | |||
32 | #video-info { | ||
33 | .video-name-views { | ||
34 | font-weight: bold; | ||
35 | font-size: 18px; | ||
36 | min-height: $video-watch-title-height; | ||
37 | display: flex; | ||
38 | align-items: center; | ||
39 | |||
40 | .video-name { | ||
41 | padding-left: $video-watch-info-padding-left; | ||
42 | } | ||
43 | 41 | ||
44 | .video-views { | 42 | .video-info-name { |
45 | text-align: right; | 43 | font-size: 27px; |
46 | // Keep a symmetry with the video name | 44 | font-weight: $font-semibold; |
47 | padding-right: $video-watch-info-padding-left | 45 | flex-grow: 1; |
48 | } | 46 | } |
49 | 47 | ||
50 | } | 48 | .video-info-actions { |
49 | .action-button { | ||
50 | @include peertube-button; | ||
51 | @include grey-button; | ||
51 | 52 | ||
52 | .video-small-blocks { | 53 | font-size: 15px; |
53 | height: $video-watch-info-height; | 54 | font-weight: $font-semibold; |
54 | color: $video-watch-info-color; | 55 | display: inline-block; |
55 | border-color: $video-watch-border-color; | 56 | padding: 0 10px 0 10px; |
56 | border-width: 1px 0px; | ||
57 | border-style: solid; | ||
58 | 57 | ||
59 | .video-small-block { | 58 | .icon { |
60 | height: $video-watch-info-height; | 59 | @include icon(21px); |
61 | display: flex; | ||
62 | flex-direction: column; | ||
63 | justify-content: center; | ||
64 | text-align: center; | ||
65 | 60 | ||
66 | a { | 61 | position: relative; |
67 | cursor: pointer; | 62 | top: -2px; |
68 | transition: color 0.3s; | ||
69 | white-space: nowrap; | ||
70 | overflow: hidden; | ||
71 | text-overflow: ellipsis; | ||
72 | |||
73 | &, &:hover { | ||
74 | color: inherit; | ||
75 | text-decoration:none; | ||
76 | } | ||
77 | 63 | ||
78 | &:hover { | 64 | &.icon-like { |
79 | color: #000 !important; | 65 | background-image: url('../../../assets/images/video/like-grey.svg'); |
80 | } | 66 | } |
81 | 67 | ||
82 | &:hover > .glyphicon { | 68 | &.icon-dislike { |
83 | opacity: 1 !important; | 69 | background-image: url('../../../assets/images/video/dislike-grey.svg'); |
84 | } | 70 | } |
85 | } | ||
86 | 71 | ||
87 | .option .glyphicon { | 72 | &.icon-share { |
88 | font-size: 22px; | 73 | background-image: url('../../../assets/images/video/share.svg'); |
89 | color: inherit; | 74 | } |
90 | opacity: 0.15; | ||
91 | margin-bottom: 10px; | ||
92 | transition: opacity 0.3s; | ||
93 | } | ||
94 | 75 | ||
95 | .video-small-block-text { | 76 | &.icon-more { |
96 | font-size: 15px; | 77 | background-image: url('../../../assets/images/video/more.svg'); |
97 | font-weight: bold; | 78 | top: -1px; |
98 | } | 79 | } |
99 | } | 80 | } |
100 | 81 | ||
101 | .video-small-block:not(:last-child) { | 82 | &.activated { |
102 | border-width: 0 1px 0 0; | 83 | @include orange-button; |
103 | border-color: $video-watch-border-color; | ||
104 | border-style: solid; | ||
105 | } | ||
106 | 84 | ||
107 | .video-small-block-account, .video-small-block-more { | 85 | .icon-like { |
108 | a.option { | 86 | background-image: url('../../../assets/images/video/like-white.svg'); |
109 | display: block; | 87 | } |
110 | 88 | ||
111 | .glyphicon { | 89 | .icon-dislike { |
112 | display: block; | 90 | background-image: url('../../../assets/images/video/dislike-white.svg'); |
91 | } | ||
92 | } | ||
113 | } | 93 | } |
114 | } | ||
115 | } | ||
116 | 94 | ||
117 | .video-small-block-share, .video-small-block-more { | 95 | .action-more { |
118 | a.option { | 96 | display: inline-block; |
119 | display: block; | 97 | |
120 | 98 | .dropdown-menu .icon { | |
121 | .glyphicon { | 99 | display: inline-block; |
122 | display: block; | 100 | background-repeat: no-repeat; |
101 | background-size: contain; | ||
102 | width: 21px; | ||
103 | height: 21px; | ||
104 | vertical-align: middle; | ||
105 | margin-right: 5px; | ||
106 | position: relative; | ||
107 | top: -1px; | ||
108 | |||
109 | &.icon-download { | ||
110 | background-image: url('../../../assets/images/video/download-grey.svg'); | ||
111 | } | ||
112 | |||
113 | &.icon-alert { | ||
114 | background-image: url('../../../assets/images/video/alert.svg'); | ||
115 | } | ||
116 | |||
117 | &.icon-blacklist { | ||
118 | background-image: url('../../../assets/images/video/eye-closed.svg'); | ||
119 | } | ||
120 | } | ||
123 | } | 121 | } |
124 | } | 122 | } |
125 | } | 123 | } |
126 | 124 | ||
127 | .video-small-block-more .video-small-block-dropdown { | 125 | .video-info-date-views-bar { |
128 | position: relative; | 126 | display: flex; |
129 | |||
130 | .dropdown-item .glyphicon { | ||
131 | margin-right: 5px; | ||
132 | } | ||
133 | } | ||
134 | |||
135 | .video-small-block-rating { | ||
136 | 127 | ||
137 | .video-small-block-like { | 128 | .video-info-date-views { |
129 | font-size: 16px; | ||
138 | margin-bottom: 10px; | 130 | margin-bottom: 10px; |
131 | flex-grow: 1; | ||
139 | } | 132 | } |
140 | 133 | ||
141 | .video-small-block-text { | 134 | .video-info-likes-dislikes-bar { |
142 | vertical-align: top; | 135 | height: 5px; |
143 | } | 136 | width: 186px; |
137 | background-color: #E5E5E5; | ||
138 | margin-top: 25px; | ||
144 | 139 | ||
145 | .glyphicon { | 140 | .likes-bar { |
146 | font-size: 18px; | 141 | height: 100%; |
147 | margin: 0 10px 0 0; | 142 | background-color: #39CC0B; |
148 | opacity: 0.3; | 143 | } |
149 | } | 144 | } |
145 | } | ||
150 | 146 | ||
151 | .interactive { | 147 | .video-info-channel { |
152 | cursor: pointer; | 148 | font-weight: $font-semibold; |
153 | transition: opacity, color 0.3s; | 149 | font-size: 15px; |
150 | } | ||
154 | 151 | ||
155 | &.activated, &:hover { | 152 | .video-info-by { |
156 | opacity: 1; | 153 | display: flex; |
157 | color: #000; | 154 | align-items: center; |
158 | } | 155 | font-size: 13px; |
156 | |||
157 | img { | ||
158 | width: 16px; | ||
159 | height: 16px; | ||
160 | margin-left: 3px; | ||
159 | } | 161 | } |
160 | } | 162 | } |
161 | } | ||
162 | 163 | ||
163 | .video-details { | 164 | .video-info-description { |
164 | margin-top: 30px; | 165 | margin: 20px 0; |
165 | 166 | font-size: 15px; | |
166 | .video-details-date-description { | ||
167 | padding-left: $video-watch-info-padding-left; | ||
168 | 167 | ||
169 | .description-loading { | 168 | .description-loading { |
170 | display: inline-block; | 169 | display: inline-block; |
171 | } | 170 | } |
172 | 171 | ||
173 | .video-details-date { | 172 | .video-info-description-more { |
174 | font-weight: bold; | ||
175 | margin-bottom: 30px; | ||
176 | } | ||
177 | |||
178 | .video-details-description-more { | ||
179 | cursor: pointer; | 173 | cursor: pointer; |
180 | margin-top: 15px; | 174 | font-weight: $font-semibold; |
181 | font-weight: bold; | 175 | color: #585858; |
182 | color: #acaeb7; | 176 | font-size: 14px; |
183 | 177 | ||
184 | .glyphicon { | 178 | .glyphicon { |
185 | position: relative; | 179 | position: relative; |
@@ -188,109 +182,68 @@ | |||
188 | } | 182 | } |
189 | } | 183 | } |
190 | 184 | ||
191 | .video-details-attributes { | 185 | .video-attributes { |
192 | font-weight: bold; | 186 | .video-attribute { |
193 | font-size: 12px; | 187 | font-size: 13px; |
194 | 188 | display: block; | |
195 | .video-details-attribute { | 189 | margin-bottom: 12px; |
196 | display: flex; | ||
197 | 190 | ||
198 | .video-details-attribute-label { | 191 | .video-attribute-label { |
199 | color: $video-watch-info-color; | 192 | width: 86px; |
200 | flex-basis: 60px; | 193 | display: inline-block; |
201 | flex-grow: 0; | 194 | color: #585858; |
202 | flex-shrink: 0; | 195 | font-weight: $font-bold; |
203 | margin-right: 5px; | ||
204 | } | 196 | } |
205 | } | 197 | } |
206 | } | 198 | } |
207 | |||
208 | .video-details-tags { | ||
209 | display: flex; | ||
210 | flex-wrap: wrap; | ||
211 | |||
212 | a { | ||
213 | margin: 0 3px 3px 0; | ||
214 | font-size: 11px; | ||
215 | } | ||
216 | } | ||
217 | } | 199 | } |
218 | 200 | ||
219 | @media screen and (max-width: 800px) { | 201 | .other-videos { |
220 | .video-name-views { | 202 | .title-page { |
221 | .video-name { | 203 | margin-top: 0; |
222 | padding-left: 5px; | ||
223 | padding-right: 0px; | ||
224 | } | ||
225 | |||
226 | .video-views { | ||
227 | padding-left: 0px; | ||
228 | padding-right: 5px; | ||
229 | } | ||
230 | } | 204 | } |
231 | 205 | ||
232 | .video-small-blocks { | 206 | /deep/ .video-miniature { |
233 | a, .video-small-block-text { | 207 | display: flex; |
234 | font-size: 13px !important; | 208 | height: 100%; |
235 | } | 209 | margin-bottom: 20px; |
236 | |||
237 | .glyphicon { | ||
238 | font-size: 18px !important; | ||
239 | } | ||
240 | 210 | ||
241 | .video-small-block-account { | 211 | .video-miniature-information { |
242 | padding-left: 10px; | 212 | margin-left: 10px; |
243 | padding-right: 10px; | ||
244 | } | 213 | } |
245 | } | 214 | } |
215 | } | ||
216 | } | ||
246 | 217 | ||
247 | .video-details { | ||
248 | .video-details-date-description { | ||
249 | padding-left: 10px; | ||
250 | font-size: 13px !important; | ||
251 | } | ||
252 | |||
253 | .video-details-attributes { | ||
254 | font-size: 11px !important; | ||
255 | 218 | ||
256 | .video-details-attribute-label { | 219 | @media screen and (max-width: 1000px) { |
257 | width: 50px; | 220 | .other-videos { |
258 | } | 221 | display: none; |
259 | } | ||
260 | } | ||
261 | } | 222 | } |
223 | } | ||
262 | 224 | ||
263 | @media screen and (max-width: 500px) { | 225 | @media screen and (max-width: 800px) { |
264 | .video-name-views { | 226 | .video-bottom { |
265 | font-size: 16px !important; | 227 | margin: 20px 0 0 0; |
266 | } | ||
267 | 228 | ||
268 | // Keep the same hierarchy than max-width: 800px | 229 | .video-info { |
269 | .video-small-blocks { | 230 | margin-right: 0; |
270 | a, .video-small-block-text { | ||
271 | font-size: 10px !important; | ||
272 | } | ||
273 | 231 | ||
274 | .video-small-block-account { | 232 | .video-info-name-actions { |
275 | padding-left: 5px; | 233 | align-items: left; |
276 | padding-right: 5px; | 234 | flex-direction: column; |
235 | margin-bottom: 30px; | ||
277 | } | 236 | } |
278 | } | ||
279 | 237 | ||
280 | .video-details { | 238 | .video-info-date-views-bar { |
281 | .video-details-date-description { | 239 | align-items: left; |
240 | flex-direction: column; | ||
282 | margin-bottom: 30px; | 241 | margin-bottom: 30px; |
283 | width: 100%; | ||
284 | 242 | ||
285 | .video-details-date { | 243 | .video-info-likes-dislikes-bar { |
286 | margin-bottom: 15px; | 244 | margin-top: 0; |
287 | } | 245 | } |
288 | } | 246 | } |
289 | |||
290 | .video-details-attributes { | ||
291 | padding-left: 10px; | ||
292 | padding-right: 10px; | ||
293 | } | ||
294 | } | 247 | } |
295 | } | 248 | } |
296 | } | 249 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index b26f3092f..d4e3ec014 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -2,6 +2,7 @@ import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/co | |||
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { MetaService } from '@ngx-meta/core' | 3 | import { MetaService } from '@ngx-meta/core' |
4 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
5 | import { VideoService } from 'app/shared/video/video.service' | ||
5 | import { Observable } from 'rxjs/Observable' | 6 | import { Observable } from 'rxjs/Observable' |
6 | import { Subscription } from 'rxjs/Subscription' | 7 | import { Subscription } from 'rxjs/Subscription' |
7 | import videojs from 'video.js' | 8 | import videojs from 'video.js' |
@@ -9,7 +10,10 @@ import { UserVideoRateType, VideoRateType } from '../../../../../shared' | |||
9 | import '../../../assets/player/peertube-videojs-plugin' | 10 | import '../../../assets/player/peertube-videojs-plugin' |
10 | import { AuthService, ConfirmService } from '../../core' | 11 | import { AuthService, ConfirmService } from '../../core' |
11 | import { VideoBlacklistService } from '../../shared' | 12 | import { VideoBlacklistService } from '../../shared' |
12 | import { MarkdownService, VideoDetails, VideoService } from '../shared' | 13 | import { Account } from '../../shared/account/account.model' |
14 | import { VideoDetails } from '../../shared/video/video-details.model' | ||
15 | import { Video } from '../../shared/video/video.model' | ||
16 | import { MarkdownService } from '../shared' | ||
13 | import { VideoDownloadComponent } from './video-download.component' | 17 | import { VideoDownloadComponent } from './video-download.component' |
14 | import { VideoReportComponent } from './video-report.component' | 18 | import { VideoReportComponent } from './video-report.component' |
15 | import { VideoShareComponent } from './video-share.component' | 19 | import { VideoShareComponent } from './video-share.component' |
@@ -24,13 +28,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
24 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent | 28 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent |
25 | @ViewChild('videoReportModal') videoReportModal: VideoReportComponent | 29 | @ViewChild('videoReportModal') videoReportModal: VideoReportComponent |
26 | 30 | ||
27 | downloadSpeed: number | 31 | otherVideos: Video[] = [] |
32 | |||
28 | error = false | 33 | error = false |
29 | loading = false | 34 | loading = false |
30 | numPeers: number | ||
31 | player: videojs.Player | 35 | player: videojs.Player |
32 | playerElement: HTMLMediaElement | 36 | playerElement: HTMLMediaElement |
33 | uploadSpeed: number | ||
34 | userRating: UserVideoRateType = null | 37 | userRating: UserVideoRateType = null |
35 | video: VideoDetails = null | 38 | video: VideoDetails = null |
36 | videoPlayerLoaded = false | 39 | videoPlayerLoaded = false |
@@ -58,6 +61,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
58 | ) {} | 61 | ) {} |
59 | 62 | ||
60 | ngOnInit () { | 63 | ngOnInit () { |
64 | this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt') | ||
65 | .subscribe( | ||
66 | data => this.otherVideos = data.videos, | ||
67 | |||
68 | err => console.error(err) | ||
69 | ) | ||
70 | |||
61 | this.paramsSub = this.route.params.subscribe(routeParams => { | 71 | this.paramsSub = this.route.params.subscribe(routeParams => { |
62 | let uuid = routeParams['uuid'] | 72 | let uuid = routeParams['uuid'] |
63 | this.videoService.getVideo(uuid).subscribe( | 73 | this.videoService.getVideo(uuid).subscribe( |
@@ -115,27 +125,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
115 | ) | 125 | ) |
116 | } | 126 | } |
117 | 127 | ||
118 | removeVideo (event: Event) { | ||
119 | event.preventDefault() | ||
120 | |||
121 | this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe( | ||
122 | res => { | ||
123 | if (res === false) return | ||
124 | |||
125 | this.videoService.removeVideo(this.video.id) | ||
126 | .subscribe( | ||
127 | status => { | ||
128 | this.notificationsService.success('Success', `Video ${this.video.name} deleted.`) | ||
129 | // Go back to the video-list. | ||
130 | this.router.navigate(['/videos/list']) | ||
131 | }, | ||
132 | |||
133 | error => this.notificationsService.error('Error', error.text) | ||
134 | ) | ||
135 | } | ||
136 | ) | ||
137 | } | ||
138 | |||
139 | blacklistVideo (event: Event) { | 128 | blacklistVideo (event: Event) { |
140 | event.preventDefault() | 129 | event.preventDefault() |
141 | 130 | ||
@@ -166,7 +155,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
166 | } | 155 | } |
167 | 156 | ||
168 | showLessDescription () { | 157 | showLessDescription () { |
169 | |||
170 | this.updateVideoDescription(this.shortVideoDescription) | 158 | this.updateVideoDescription(this.shortVideoDescription) |
171 | this.completeDescriptionShown = false | 159 | this.completeDescriptionShown = false |
172 | } | 160 | } |
@@ -211,16 +199,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
211 | return this.authService.isLoggedIn() | 199 | return this.authService.isLoggedIn() |
212 | } | 200 | } |
213 | 201 | ||
214 | canUserUpdateVideo () { | 202 | isVideoBlacklistable () { |
215 | return this.video.isUpdatableBy(this.authService.getUser()) | 203 | return this.video.isBlackistableBy(this.authService.getUser()) |
216 | } | 204 | } |
217 | 205 | ||
218 | isVideoRemovable () { | 206 | getAvatarPath () { |
219 | return this.video.isRemovableBy(this.authService.getUser()) | 207 | return Account.GET_ACCOUNT_AVATAR_PATH(this.video.account) |
220 | } | 208 | } |
221 | 209 | ||
222 | isVideoBlacklistable () { | 210 | getVideoTags () { |
223 | return this.video.isBlackistableBy(this.authService.getUser()) | 211 | if (!this.video || Array.isArray(this.video.tags) === false) return [] |
212 | |||
213 | return this.video.tags.join(', ') | ||
224 | } | 214 | } |
225 | 215 | ||
226 | private updateVideoDescription (description: string) { | 216 | private updateVideoDescription (description: string) { |
@@ -229,6 +219,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
229 | } | 219 | } |
230 | 220 | ||
231 | private setVideoDescriptionHTML () { | 221 | private setVideoDescriptionHTML () { |
222 | if (!this.video.description) { | ||
223 | this.videoHTMLDescription = '' | ||
224 | return | ||
225 | } | ||
226 | |||
232 | this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description) | 227 | this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description) |
233 | } | 228 | } |
234 | 229 | ||
@@ -281,7 +276,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
281 | return this.router.navigate([ '/videos/list' ]) | 276 | return this.router.navigate([ '/videos/list' ]) |
282 | } | 277 | } |
283 | 278 | ||
284 | this.playerElement = this.elementRef.nativeElement.querySelector('#video-container') | 279 | this.playerElement = this.elementRef.nativeElement.querySelector('#video-element') |
285 | 280 | ||
286 | const videojsOptions = { | 281 | const videojsOptions = { |
287 | controls: true, | 282 | controls: true, |
@@ -304,12 +299,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
304 | this.on('customError', (event, data) => { | 299 | this.on('customError', (event, data) => { |
305 | self.handleError(data.err) | 300 | self.handleError(data.err) |
306 | }) | 301 | }) |
307 | |||
308 | this.on('torrentInfo', (event, data) => { | ||
309 | self.downloadSpeed = data.downloadSpeed | ||
310 | self.numPeers = data.numPeers | ||
311 | self.uploadSpeed = data.uploadSpeed | ||
312 | }) | ||
313 | }) | 302 | }) |
314 | 303 | ||
315 | this.setVideoDescriptionHTML() | 304 | this.setVideoDescriptionHTML() |
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts index 1b983200d..0b1dd5c15 100644 --- a/client/src/app/videos/+video-watch/video-watch.module.ts +++ b/client/src/app/videos/+video-watch/video-watch.module.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | 2 | ||
3 | import { VideoWatchRoutingModule } from './video-watch-routing.module' | 3 | import { VideoWatchRoutingModule } from './video-watch-routing.module' |
4 | import { VideoService, MarkdownService } from '../shared' | 4 | import { MarkdownService } from '../shared' |
5 | import { SharedModule } from '../../shared' | 5 | import { SharedModule } from '../../shared' |
6 | 6 | ||
7 | import { VideoWatchComponent } from './video-watch.component' | 7 | import { VideoWatchComponent } from './video-watch.component' |
@@ -28,8 +28,7 @@ import { VideoDownloadComponent } from './video-download.component' | |||
28 | ], | 28 | ], |
29 | 29 | ||
30 | providers: [ | 30 | providers: [ |
31 | MarkdownService, | 31 | MarkdownService |
32 | VideoService | ||
33 | ] | 32 | ] |
34 | }) | 33 | }) |
35 | export class VideoWatchModule { } | 34 | export class VideoWatchModule { } |
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts index 3f1458088..7a66944b9 100644 --- a/client/src/app/videos/shared/index.ts +++ b/client/src/app/videos/shared/index.ts | |||
@@ -1,8 +1 @@ | |||
1 | export * from './sort-field.type' | ||
2 | export * from './markdown.service' | export * from './markdown.service' | |
3 | export * from './video.model' | ||
4 | export * from './video-details.model' | ||
5 | export * from './video-edit.model' | ||
6 | export * from './video.service' | ||
7 | export * from './video-description.component' | ||
8 | export * from './video-pagination.model' | ||
diff --git a/client/src/app/videos/shared/sort-field.type.ts b/client/src/app/videos/shared/sort-field.type.ts deleted file mode 100644 index 776f360f8..000000000 --- a/client/src/app/videos/shared/sort-field.type.ts +++ /dev/null | |||
@@ -1,5 +0,0 @@ | |||
1 | export type SortField = 'name' | '-name' | ||
2 | | 'duration' | '-duration' | ||
3 | | 'createdAt' | '-createdAt' | ||
4 | | 'views' | '-views' | ||
5 | | 'likes' | '-likes' | ||
diff --git a/client/src/app/videos/shared/video-description.component.scss b/client/src/app/videos/shared/video-description.component.scss deleted file mode 100644 index d8d73e846..000000000 --- a/client/src/app/videos/shared/video-description.component.scss +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | textarea { | ||
2 | height: 150px; | ||
3 | } | ||
4 | |||
5 | .previews /deep/ { | ||
6 | .nav { | ||
7 | margin-top: 10px; | ||
8 | font-size: 0.9em; | ||
9 | } | ||
10 | |||
11 | .tab-content { | ||
12 | min-height: 75px; | ||
13 | padding: 5px; | ||
14 | } | ||
15 | } | ||
diff --git a/client/src/app/videos/shared/video-details.model.ts b/client/src/app/videos/shared/video-details.model.ts deleted file mode 100644 index 64cb4f847..000000000 --- a/client/src/app/videos/shared/video-details.model.ts +++ /dev/null | |||
@@ -1,84 +0,0 @@ | |||
1 | import { Video } from './video.model' | ||
2 | import { AuthUser } from '../../core' | ||
3 | import { | ||
4 | VideoDetails as VideoDetailsServerModel, | ||
5 | VideoFile, | ||
6 | VideoChannel, | ||
7 | VideoResolution, | ||
8 | UserRight, | ||
9 | VideoPrivacy | ||
10 | } from '../../../../../shared' | ||
11 | |||
12 | export class VideoDetails extends Video implements VideoDetailsServerModel { | ||
13 | account: string | ||
14 | by: string | ||
15 | createdAt: Date | ||
16 | updatedAt: Date | ||
17 | categoryLabel: string | ||
18 | category: number | ||
19 | licenceLabel: string | ||
20 | licence: number | ||
21 | languageLabel: string | ||
22 | language: number | ||
23 | description: string | ||
24 | duration: number | ||
25 | durationLabel: string | ||
26 | id: number | ||
27 | uuid: string | ||
28 | isLocal: boolean | ||
29 | name: string | ||
30 | serverHost: string | ||
31 | tags: string[] | ||
32 | thumbnailPath: string | ||
33 | thumbnailUrl: string | ||
34 | previewPath: string | ||
35 | previewUrl: string | ||
36 | embedPath: string | ||
37 | embedUrl: string | ||
38 | views: number | ||
39 | likes: number | ||
40 | dislikes: number | ||
41 | nsfw: boolean | ||
42 | descriptionPath: string | ||
43 | files: VideoFile[] | ||
44 | channel: VideoChannel | ||
45 | privacy: VideoPrivacy | ||
46 | privacyLabel: string | ||
47 | |||
48 | constructor (hash: VideoDetailsServerModel) { | ||
49 | super(hash) | ||
50 | |||
51 | this.privacy = hash.privacy | ||
52 | this.privacyLabel = hash.privacyLabel | ||
53 | this.descriptionPath = hash.descriptionPath | ||
54 | this.files = hash.files | ||
55 | this.channel = hash.channel | ||
56 | } | ||
57 | |||
58 | getAppropriateMagnetUri (actualDownloadSpeed = 0) { | ||
59 | if (this.files === undefined || this.files.length === 0) return '' | ||
60 | if (this.files.length === 1) return this.files[0].magnetUri | ||
61 | |||
62 | // Find first video that is good for our download speed (remember they are sorted) | ||
63 | let betterResolutionFile = this.files.find(f => actualDownloadSpeed > (f.size / this.duration)) | ||
64 | |||
65 | // If the download speed is too bad, return the lowest resolution we have | ||
66 | if (betterResolutionFile === undefined) { | ||
67 | betterResolutionFile = this.files.find(f => f.resolution === VideoResolution.H_240P) | ||
68 | } | ||
69 | |||
70 | return betterResolutionFile.magnetUri | ||
71 | } | ||
72 | |||
73 | isRemovableBy (user: AuthUser) { | ||
74 | return user && this.isLocal === true && (this.account === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) | ||
75 | } | ||
76 | |||
77 | isBlackistableBy (user: AuthUser) { | ||
78 | return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true && this.isLocal === false | ||
79 | } | ||
80 | |||
81 | isUpdatableBy (user: AuthUser) { | ||
82 | return user && this.isLocal === true && user.username === this.account | ||
83 | } | ||
84 | } | ||
diff --git a/client/src/app/videos/shared/video-edit.model.ts b/client/src/app/videos/shared/video-edit.model.ts deleted file mode 100644 index 88d23a59f..000000000 --- a/client/src/app/videos/shared/video-edit.model.ts +++ /dev/null | |||
@@ -1,50 +0,0 @@ | |||
1 | import { VideoDetails } from './video-details.model' | ||
2 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' | ||
3 | |||
4 | export class VideoEdit { | ||
5 | category: number | ||
6 | licence: number | ||
7 | language: number | ||
8 | description: string | ||
9 | name: string | ||
10 | tags: string[] | ||
11 | nsfw: boolean | ||
12 | channel: number | ||
13 | privacy: VideoPrivacy | ||
14 | uuid?: string | ||
15 | id?: number | ||
16 | |||
17 | constructor (videoDetails: VideoDetails) { | ||
18 | this.id = videoDetails.id | ||
19 | this.uuid = videoDetails.uuid | ||
20 | this.category = videoDetails.category | ||
21 | this.licence = videoDetails.licence | ||
22 | this.language = videoDetails.language | ||
23 | this.description = videoDetails.description | ||
24 | this.name = videoDetails.name | ||
25 | this.tags = videoDetails.tags | ||
26 | this.nsfw = videoDetails.nsfw | ||
27 | this.channel = videoDetails.channel.id | ||
28 | this.privacy = videoDetails.privacy | ||
29 | } | ||
30 | |||
31 | patch (values: Object) { | ||
32 | Object.keys(values).forEach((key) => { | ||
33 | this[key] = values[key] | ||
34 | }) | ||
35 | } | ||
36 | |||
37 | toJSON () { | ||
38 | return { | ||
39 | category: this.category, | ||
40 | licence: this.licence, | ||
41 | language: this.language, | ||
42 | description: this.description, | ||
43 | name: this.name, | ||
44 | tags: this.tags, | ||
45 | nsfw: this.nsfw, | ||
46 | channel: this.channel, | ||
47 | privacy: this.privacy | ||
48 | } | ||
49 | } | ||
50 | } | ||
diff --git a/client/src/app/videos/shared/video-pagination.model.ts b/client/src/app/videos/shared/video-pagination.model.ts deleted file mode 100644 index 9e71769cb..000000000 --- a/client/src/app/videos/shared/video-pagination.model.ts +++ /dev/null | |||
@@ -1,5 +0,0 @@ | |||
1 | export interface VideoPagination { | ||
2 | currentPage: number | ||
3 | itemsPerPage: number | ||
4 | totalItems: number | ||
5 | } | ||
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts deleted file mode 100644 index 0dd41d71b..000000000 --- a/client/src/app/videos/shared/video.model.ts +++ /dev/null | |||
@@ -1,90 +0,0 @@ | |||
1 | import { Video as VideoServerModel } from '../../../../../shared' | ||
2 | import { User } from '../../shared' | ||
3 | |||
4 | export class Video implements VideoServerModel { | ||
5 | account: string | ||
6 | by: string | ||
7 | createdAt: Date | ||
8 | updatedAt: Date | ||
9 | categoryLabel: string | ||
10 | category: number | ||
11 | licenceLabel: string | ||
12 | licence: number | ||
13 | languageLabel: string | ||
14 | language: number | ||
15 | description: string | ||
16 | duration: number | ||
17 | durationLabel: string | ||
18 | id: number | ||
19 | uuid: string | ||
20 | isLocal: boolean | ||
21 | name: string | ||
22 | serverHost: string | ||
23 | tags: string[] | ||
24 | thumbnailPath: string | ||
25 | thumbnailUrl: string | ||
26 | previewPath: string | ||
27 | previewUrl: string | ||
28 | embedPath: string | ||
29 | embedUrl: string | ||
30 | views: number | ||
31 | likes: number | ||
32 | dislikes: number | ||
33 | nsfw: boolean | ||
34 | |||
35 | private static createByString (account: string, serverHost: string) { | ||
36 | return account + '@' + serverHost | ||
37 | } | ||
38 | |||
39 | private static createDurationString (duration: number) { | ||
40 | const minutes = Math.floor(duration / 60) | ||
41 | const seconds = duration % 60 | ||
42 | const minutesPadding = minutes >= 10 ? '' : '0' | ||
43 | const secondsPadding = seconds >= 10 ? '' : '0' | ||
44 | |||
45 | return minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString() | ||
46 | } | ||
47 | |||
48 | constructor (hash: VideoServerModel) { | ||
49 | let absoluteAPIUrl = API_URL | ||
50 | if (!absoluteAPIUrl) { | ||
51 | // The API is on the same domain | ||
52 | absoluteAPIUrl = window.location.origin | ||
53 | } | ||
54 | |||
55 | this.account = hash.account | ||
56 | this.createdAt = new Date(hash.createdAt.toString()) | ||
57 | this.categoryLabel = hash.categoryLabel | ||
58 | this.category = hash.category | ||
59 | this.licenceLabel = hash.licenceLabel | ||
60 | this.licence = hash.licence | ||
61 | this.languageLabel = hash.languageLabel | ||
62 | this.language = hash.language | ||
63 | this.description = hash.description | ||
64 | this.duration = hash.duration | ||
65 | this.durationLabel = Video.createDurationString(hash.duration) | ||
66 | this.id = hash.id | ||
67 | this.uuid = hash.uuid | ||
68 | this.isLocal = hash.isLocal | ||
69 | this.name = hash.name | ||
70 | this.serverHost = hash.serverHost | ||
71 | this.tags = hash.tags | ||
72 | this.thumbnailPath = hash.thumbnailPath | ||
73 | this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath | ||
74 | this.previewPath = hash.previewPath | ||
75 | this.previewUrl = absoluteAPIUrl + hash.previewPath | ||
76 | this.embedPath = hash.embedPath | ||
77 | this.embedUrl = absoluteAPIUrl + hash.embedPath | ||
78 | this.views = hash.views | ||
79 | this.likes = hash.likes | ||
80 | this.dislikes = hash.dislikes | ||
81 | this.nsfw = hash.nsfw | ||
82 | |||
83 | this.by = Video.createByString(hash.account, hash.serverHost) | ||
84 | } | ||
85 | |||
86 | isVideoNSFWForUser (user: User) { | ||
87 | // If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos... | ||
88 | return (this.nsfw && (!user || user.displayNSFW === false)) | ||
89 | } | ||
90 | } | ||
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts deleted file mode 100644 index 5d25a26d4..000000000 --- a/client/src/app/videos/shared/video.service.ts +++ /dev/null | |||
@@ -1,176 +0,0 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { Observable } from 'rxjs/Observable' | ||
3 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' | ||
4 | import 'rxjs/add/operator/catch' | ||
5 | import 'rxjs/add/operator/map' | ||
6 | |||
7 | import { SortField } from './sort-field.type' | ||
8 | import { | ||
9 | RestExtractor, | ||
10 | RestService, | ||
11 | UserService, | ||
12 | Search | ||
13 | } from '../../shared' | ||
14 | import { Video } from './video.model' | ||
15 | import { VideoDetails } from './video-details.model' | ||
16 | import { VideoEdit } from './video-edit.model' | ||
17 | import { VideoPagination } from './video-pagination.model' | ||
18 | import { | ||
19 | UserVideoRate, | ||
20 | VideoRateType, | ||
21 | VideoUpdate, | ||
22 | UserVideoRateUpdate, | ||
23 | Video as VideoServerModel, | ||
24 | VideoDetails as VideoDetailsServerModel, | ||
25 | ResultList | ||
26 | } from '../../../../../shared' | ||
27 | |||
28 | @Injectable() | ||
29 | export class VideoService { | ||
30 | private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/' | ||
31 | |||
32 | constructor ( | ||
33 | private authHttp: HttpClient, | ||
34 | private restExtractor: RestExtractor, | ||
35 | private restService: RestService | ||
36 | ) {} | ||
37 | |||
38 | getVideo (uuid: string): Observable<VideoDetails> { | ||
39 | return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid) | ||
40 | .map(videoHash => new VideoDetails(videoHash)) | ||
41 | .catch((res) => this.restExtractor.handleError(res)) | ||
42 | } | ||
43 | |||
44 | viewVideo (uuid: string): Observable<VideoDetails> { | ||
45 | return this.authHttp.post(VideoService.BASE_VIDEO_URL + uuid + '/views', {}) | ||
46 | .map(this.restExtractor.extractDataBool) | ||
47 | .catch(this.restExtractor.handleError) | ||
48 | } | ||
49 | |||
50 | updateVideo (video: VideoEdit) { | ||
51 | const language = video.language ? video.language : null | ||
52 | |||
53 | const body: VideoUpdate = { | ||
54 | name: video.name, | ||
55 | category: video.category, | ||
56 | licence: video.licence, | ||
57 | language, | ||
58 | description: video.description, | ||
59 | privacy: video.privacy, | ||
60 | tags: video.tags, | ||
61 | nsfw: video.nsfw | ||
62 | } | ||
63 | |||
64 | return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, body) | ||
65 | .map(this.restExtractor.extractDataBool) | ||
66 | .catch(this.restExtractor.handleError) | ||
67 | } | ||
68 | |||
69 | uploadVideo (video: FormData) { | ||
70 | const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true }) | ||
71 | |||
72 | return this.authHttp | ||
73 | .request(req) | ||
74 | .catch(this.restExtractor.handleError) | ||
75 | } | ||
76 | |||
77 | getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { | ||
78 | const pagination = this.videoPaginationToRestPagination(videoPagination) | ||
79 | |||
80 | let params = new HttpParams() | ||
81 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
82 | |||
83 | return this.authHttp.get(UserService.BASE_USERS_URL + '/me/videos', { params }) | ||
84 | .map(this.extractVideos) | ||
85 | .catch((res) => this.restExtractor.handleError(res)) | ||
86 | } | ||
87 | |||
88 | getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { | ||
89 | const pagination = this.videoPaginationToRestPagination(videoPagination) | ||
90 | |||
91 | let params = new HttpParams() | ||
92 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
93 | |||
94 | return this.authHttp | ||
95 | .get(VideoService.BASE_VIDEO_URL, { params }) | ||
96 | .map(this.extractVideos) | ||
97 | .catch((res) => this.restExtractor.handleError(res)) | ||
98 | } | ||
99 | |||
100 | searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { | ||
101 | const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value) | ||
102 | |||
103 | const pagination = this.videoPaginationToRestPagination(videoPagination) | ||
104 | |||
105 | let params = new HttpParams() | ||
106 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
107 | |||
108 | if (search.field) params.set('field', search.field) | ||
109 | |||
110 | return this.authHttp | ||
111 | .get<ResultList<VideoServerModel>>(url, { params }) | ||
112 | .map(this.extractVideos) | ||
113 | .catch((res) => this.restExtractor.handleError(res)) | ||
114 | } | ||
115 | |||
116 | removeVideo (id: number) { | ||
117 | return this.authHttp | ||
118 | .delete(VideoService.BASE_VIDEO_URL + id) | ||
119 | .map(this.restExtractor.extractDataBool) | ||
120 | .catch((res) => this.restExtractor.handleError(res)) | ||
121 | } | ||
122 | |||
123 | loadCompleteDescription (descriptionPath: string) { | ||
124 | return this.authHttp | ||
125 | .get(API_URL + descriptionPath) | ||
126 | .map(res => res['description']) | ||
127 | .catch((res) => this.restExtractor.handleError(res)) | ||
128 | } | ||
129 | |||
130 | setVideoLike (id: number) { | ||
131 | return this.setVideoRate(id, 'like') | ||
132 | } | ||
133 | |||
134 | setVideoDislike (id: number) { | ||
135 | return this.setVideoRate(id, 'dislike') | ||
136 | } | ||
137 | |||
138 | getUserVideoRating (id: number): Observable<UserVideoRate> { | ||
139 | const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' | ||
140 | |||
141 | return this.authHttp | ||
142 | .get(url) | ||
143 | .catch(res => this.restExtractor.handleError(res)) | ||
144 | } | ||
145 | |||
146 | private videoPaginationToRestPagination (videoPagination: VideoPagination) { | ||
147 | const start: number = (videoPagination.currentPage - 1) * videoPagination.itemsPerPage | ||
148 | const count: number = videoPagination.itemsPerPage | ||
149 | |||
150 | return { start, count } | ||
151 | } | ||
152 | |||
153 | private setVideoRate (id: number, rateType: VideoRateType) { | ||
154 | const url = VideoService.BASE_VIDEO_URL + id + '/rate' | ||
155 | const body: UserVideoRateUpdate = { | ||
156 | rating: rateType | ||
157 | } | ||
158 | |||
159 | return this.authHttp | ||
160 | .put(url, body) | ||
161 | .map(this.restExtractor.extractDataBool) | ||
162 | .catch(res => this.restExtractor.handleError(res)) | ||
163 | } | ||
164 | |||
165 | private extractVideos (result: ResultList<VideoServerModel>) { | ||
166 | const videosJson = result.data | ||
167 | const totalVideos = result.total | ||
168 | const videos = [] | ||
169 | |||
170 | for (const videoJson of videosJson) { | ||
171 | videos.push(new Video(videoJson)) | ||
172 | } | ||
173 | |||
174 | return { videos, totalVideos } | ||
175 | } | ||
176 | } | ||
diff --git a/client/src/app/videos/video-list/index.ts b/client/src/app/videos/video-list/index.ts index ed2bb1657..5e7c7886c 100644 --- a/client/src/app/videos/video-list/index.ts +++ b/client/src/app/videos/video-list/index.ts | |||
@@ -1,3 +1,3 @@ | |||
1 | export * from './my-videos.component' | 1 | export * from './video-recently-added.component' |
2 | export * from './video-list.component' | 2 | export * from './video-trending.component' |
3 | export * from './shared' | 3 | export * from './video-search.component' |
diff --git a/client/src/app/videos/video-list/my-videos.component.ts b/client/src/app/videos/video-list/my-videos.component.ts deleted file mode 100644 index 648741a40..000000000 --- a/client/src/app/videos/video-list/my-videos.component.ts +++ /dev/null | |||
@@ -1,36 +0,0 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | |||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | |||
6 | import { AbstractVideoList } from './shared' | ||
7 | import { VideoService } from '../shared' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-videos', | ||
11 | styleUrls: [ './shared/abstract-video-list.scss' ], | ||
12 | templateUrl: './shared/abstract-video-list.html' | ||
13 | }) | ||
14 | export class MyVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | ||
15 | |||
16 | constructor ( | ||
17 | protected router: Router, | ||
18 | protected route: ActivatedRoute, | ||
19 | protected notificationsService: NotificationsService, | ||
20 | private videoService: VideoService | ||
21 | ) { | ||
22 | super() | ||
23 | } | ||
24 | |||
25 | ngOnInit () { | ||
26 | super.ngOnInit() | ||
27 | } | ||
28 | |||
29 | ngOnDestroy () { | ||
30 | this.subActivatedRoute.unsubscribe() | ||
31 | } | ||
32 | |||
33 | getVideosObservable () { | ||
34 | return this.videoService.getMyVideos(this.pagination, this.sort) | ||
35 | } | ||
36 | } | ||
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.html b/client/src/app/videos/video-list/shared/abstract-video-list.html deleted file mode 100644 index 680fba3f5..000000000 --- a/client/src/app/videos/video-list/shared/abstract-video-list.html +++ /dev/null | |||
@@ -1,28 +0,0 @@ | |||
1 | <div class="row"> | ||
2 | <div class="content-padding"> | ||
3 | <div class="videos-info"> | ||
4 | <div class="col-md-9 col-xs-5 videos-total-results"> | ||
5 | <span *ngIf="pagination.totalItems !== null">{{ pagination.totalItems }} videos</span> | ||
6 | |||
7 | <my-loader [loading]="loading | async"></my-loader> | ||
8 | </div> | ||
9 | |||
10 | <my-video-sort class="col-md-3 col-xs-7" [currentSort]="sort" (sort)="onSort($event)"></my-video-sort> | ||
11 | </div> | ||
12 | </div> | ||
13 | </div> | ||
14 | |||
15 | <div class="content-padding videos-miniatures"> | ||
16 | <div class="no-video" *ngIf="isThereNoVideo()">There is no video.</div> | ||
17 | |||
18 | <my-video-miniature | ||
19 | class="ng-animate" | ||
20 | *ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort" | ||
21 | > | ||
22 | </my-video-miniature> | ||
23 | </div> | ||
24 | |||
25 | <pagination *ngIf="pagination.totalItems !== null && pagination.totalItems !== 0" | ||
26 | [totalItems]="pagination.totalItems" [itemsPerPage]="pagination.itemsPerPage" [maxSize]="6" [boundaryLinks]="true" [rotate]="false" | ||
27 | [(ngModel)]="pagination.currentPage" (pageChanged)="onPageChanged($event)" | ||
28 | ></pagination> | ||
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.scss b/client/src/app/videos/video-list/shared/abstract-video-list.scss deleted file mode 100644 index 4b4409602..000000000 --- a/client/src/app/videos/video-list/shared/abstract-video-list.scss +++ /dev/null | |||
@@ -1,37 +0,0 @@ | |||
1 | .videos-info { | ||
2 | @media screen and (max-width: 400px) { | ||
3 | margin-left: 0; | ||
4 | } | ||
5 | |||
6 | border-bottom: 1px solid #f1f1f1; | ||
7 | height: 40px; | ||
8 | line-height: 40px; | ||
9 | |||
10 | .videos-total-results { | ||
11 | font-size: 13px; | ||
12 | } | ||
13 | |||
14 | my-loader { | ||
15 | display: inline-block; | ||
16 | margin-left: 5px; | ||
17 | } | ||
18 | } | ||
19 | |||
20 | .videos-miniatures { | ||
21 | text-align: center; | ||
22 | padding-top: 0; | ||
23 | |||
24 | my-video-miniature { | ||
25 | text-align: left; | ||
26 | } | ||
27 | |||
28 | .no-video { | ||
29 | margin-top: 50px; | ||
30 | text-align: center; | ||
31 | } | ||
32 | } | ||
33 | |||
34 | pagination { | ||
35 | display: block; | ||
36 | text-align: center; | ||
37 | } | ||
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.ts b/client/src/app/videos/video-list/shared/abstract-video-list.ts deleted file mode 100644 index 87d5bc48a..000000000 --- a/client/src/app/videos/video-list/shared/abstract-video-list.ts +++ /dev/null | |||
@@ -1,104 +0,0 @@ | |||
1 | import { OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subscription } from 'rxjs/Subscription' | ||
4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject' | ||
5 | import { Observable } from 'rxjs/Observable' | ||
6 | |||
7 | import { NotificationsService } from 'angular2-notifications' | ||
8 | |||
9 | import { | ||
10 | SortField, | ||
11 | Video, | ||
12 | VideoPagination | ||
13 | } from '../../shared' | ||
14 | |||
15 | export abstract class AbstractVideoList implements OnInit, OnDestroy { | ||
16 | loading: BehaviorSubject<boolean> = new BehaviorSubject(false) | ||
17 | pagination: VideoPagination = { | ||
18 | currentPage: 1, | ||
19 | itemsPerPage: 25, | ||
20 | totalItems: null | ||
21 | } | ||
22 | sort: SortField | ||
23 | videos: Video[] = [] | ||
24 | |||
25 | protected notificationsService: NotificationsService | ||
26 | protected router: Router | ||
27 | protected route: ActivatedRoute | ||
28 | |||
29 | protected subActivatedRoute: Subscription | ||
30 | |||
31 | abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}> | ||
32 | |||
33 | ngOnInit () { | ||
34 | // Subscribe to route changes | ||
35 | this.subActivatedRoute = this.route.params.subscribe(routeParams => { | ||
36 | this.loadRouteParams(routeParams) | ||
37 | |||
38 | this.getVideos() | ||
39 | }) | ||
40 | } | ||
41 | |||
42 | ngOnDestroy () { | ||
43 | this.subActivatedRoute.unsubscribe() | ||
44 | } | ||
45 | |||
46 | getVideos () { | ||
47 | this.loading.next(true) | ||
48 | this.videos = [] | ||
49 | |||
50 | const observable = this.getVideosObservable() | ||
51 | |||
52 | observable.subscribe( | ||
53 | ({ videos, totalVideos }) => { | ||
54 | this.videos = videos | ||
55 | this.pagination.totalItems = totalVideos | ||
56 | |||
57 | this.loading.next(false) | ||
58 | }, | ||
59 | error => this.notificationsService.error('Error', error.text) | ||
60 | ) | ||
61 | } | ||
62 | |||
63 | isThereNoVideo () { | ||
64 | return !this.loading.getValue() && this.videos.length === 0 | ||
65 | } | ||
66 | |||
67 | onPageChanged (event: { page: number }) { | ||
68 | // Be sure the current page is set | ||
69 | this.pagination.currentPage = event.page | ||
70 | |||
71 | this.navigateToNewParams() | ||
72 | } | ||
73 | |||
74 | onSort (sort: SortField) { | ||
75 | this.sort = sort | ||
76 | |||
77 | this.navigateToNewParams() | ||
78 | } | ||
79 | |||
80 | protected buildRouteParams () { | ||
81 | // There is always a sort and a current page | ||
82 | const params = { | ||
83 | sort: this.sort, | ||
84 | page: this.pagination.currentPage | ||
85 | } | ||
86 | |||
87 | return params | ||
88 | } | ||
89 | |||
90 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { | ||
91 | this.sort = routeParams['sort'] as SortField || '-createdAt' | ||
92 | |||
93 | if (routeParams['page'] !== undefined) { | ||
94 | this.pagination.currentPage = parseInt(routeParams['page'], 10) | ||
95 | } else { | ||
96 | this.pagination.currentPage = 1 | ||
97 | } | ||
98 | } | ||
99 | |||
100 | protected navigateToNewParams () { | ||
101 | const routeParams = this.buildRouteParams() | ||
102 | this.router.navigate([ '/videos/list', routeParams ]) | ||
103 | } | ||
104 | } | ||
diff --git a/client/src/app/videos/video-list/shared/index.ts b/client/src/app/videos/video-list/shared/index.ts deleted file mode 100644 index d8f73bcda..000000000 --- a/client/src/app/videos/video-list/shared/index.ts +++ /dev/null | |||
@@ -1,3 +0,0 @@ | |||
1 | export * from './abstract-video-list' | ||
2 | export * from './video-miniature.component' | ||
3 | export * from './video-sort.component' | ||
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.html b/client/src/app/videos/video-list/shared/video-miniature.component.html deleted file mode 100644 index 6bbd29666..000000000 --- a/client/src/app/videos/video-list/shared/video-miniature.component.html +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | <div class="video-miniature"> | ||
2 | <a | ||
3 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.description" | ||
4 | class="video-miniature-thumbnail" | ||
5 | > | ||
6 | <img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" /> | ||
7 | |||
8 | <div class="video-miniature-thumbnail-overlay"> | ||
9 | <span class="video-miniature-thumbnail-overlay-views">{{ video.views }} views</span> | ||
10 | <span class="video-miniature-thumbnail-overlay-duration">{{ video.durationLabel }}</span> | ||
11 | </div> | ||
12 | </a> | ||
13 | |||
14 | <div class="video-miniature-information"> | ||
15 | <span class="video-miniature-name"> | ||
16 | <a | ||
17 | class="video-miniature-name" | ||
18 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" | ||
19 | > | ||
20 | {{ video.name }} | ||
21 | </a> | ||
22 | </span> | ||
23 | |||
24 | <div class="video-miniature-tags"> | ||
25 | <span *ngFor="let tag of video.tags" class="video-miniature-tag"> | ||
26 | <a [routerLink]="['/videos/list', { field: 'tags', search: tag, sort: currentSort }]" class="label label-primary">{{ tag }}</a> | ||
27 | </span> | ||
28 | </div> | ||
29 | |||
30 | <a [routerLink]="['/videos/list', { field: 'account', search: video.account, sort: currentSort }]" class="video-miniature-account">{{ video.by }}</a> | ||
31 | <span class="video-miniature-created-at">{{ video.createdAt | date:'short' }}</span> | ||
32 | </div> | ||
33 | </div> | ||
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.scss b/client/src/app/videos/video-list/shared/video-miniature.component.scss deleted file mode 100644 index 507ace098..000000000 --- a/client/src/app/videos/video-list/shared/video-miniature.component.scss +++ /dev/null | |||
@@ -1,101 +0,0 @@ | |||
1 | .video-miniature { | ||
2 | margin: 15px 10px; | ||
3 | display: inline-block; | ||
4 | position: relative; | ||
5 | height: 190px; | ||
6 | vertical-align: top; | ||
7 | |||
8 | .video-miniature-thumbnail { | ||
9 | display: inline-block; | ||
10 | position: relative; | ||
11 | border-radius: 3px; | ||
12 | overflow: hidden; | ||
13 | |||
14 | &:hover { | ||
15 | text-decoration: none !important; | ||
16 | } | ||
17 | |||
18 | img.blur-filter { | ||
19 | filter: blur(5px); | ||
20 | transform : scale(1.03); | ||
21 | } | ||
22 | |||
23 | .video-miniature-thumbnail-overlay { | ||
24 | position: absolute; | ||
25 | right: 0px; | ||
26 | bottom: 0px; | ||
27 | display: inline-block; | ||
28 | background-color: rgba(0, 0, 0, 0.7); | ||
29 | color: #fff; | ||
30 | padding: 3px 5px; | ||
31 | font-size: 11px; | ||
32 | font-weight: bold; | ||
33 | width: 100%; | ||
34 | |||
35 | .video-miniature-thumbnail-overlay-views { | ||
36 | |||
37 | } | ||
38 | |||
39 | .video-miniature-thumbnail-overlay-duration { | ||
40 | float: right; | ||
41 | } | ||
42 | } | ||
43 | } | ||
44 | |||
45 | .video-miniature-information { | ||
46 | width: 200px; | ||
47 | |||
48 | .video-miniature-name { | ||
49 | height: 23px; | ||
50 | display: block; | ||
51 | overflow: hidden; | ||
52 | text-overflow: ellipsis; | ||
53 | white-space: nowrap; | ||
54 | font-weight: bold; | ||
55 | transition: color 0.2s; | ||
56 | font-size: 15px; | ||
57 | |||
58 | &:hover { | ||
59 | text-decoration: none; | ||
60 | } | ||
61 | |||
62 | &.blur-filter { | ||
63 | filter: blur(3px); | ||
64 | padding-left: 4px; | ||
65 | } | ||
66 | |||
67 | .video-miniature-tags { | ||
68 | // Fix for chrome when tags are long | ||
69 | width: 201px; | ||
70 | |||
71 | .video-miniature-tag { | ||
72 | font-size: 13px; | ||
73 | cursor: pointer; | ||
74 | position: relative; | ||
75 | top: -2px; | ||
76 | |||
77 | .label { | ||
78 | transition: background-color 0.2s; | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | |||
84 | .video-miniature-account, .video-miniature-created-at { | ||
85 | display: block; | ||
86 | margin-left: 1px; | ||
87 | font-size: 11px; | ||
88 | color: $video-miniature-other-infos; | ||
89 | opacity: 0.9; | ||
90 | } | ||
91 | |||
92 | .video-miniature-account { | ||
93 | transition: color 0.2s; | ||
94 | |||
95 | &:hover { | ||
96 | color: #23527c; | ||
97 | text-decoration: none; | ||
98 | } | ||
99 | } | ||
100 | } | ||
101 | } | ||
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.ts b/client/src/app/videos/video-list/shared/video-miniature.component.ts deleted file mode 100644 index e5a87907b..000000000 --- a/client/src/app/videos/video-list/shared/video-miniature.component.ts +++ /dev/null | |||
@@ -1,19 +0,0 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | import { SortField, Video } from '../../shared' | ||
4 | import { User } from '../../../shared' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-video-miniature', | ||
8 | styleUrls: [ './video-miniature.component.scss' ], | ||
9 | templateUrl: './video-miniature.component.html' | ||
10 | }) | ||
11 | export class VideoMiniatureComponent { | ||
12 | @Input() currentSort: SortField | ||
13 | @Input() user: User | ||
14 | @Input() video: Video | ||
15 | |||
16 | isVideoNSFWForThisUser () { | ||
17 | return this.video.isVideoNSFWForUser(this.user) | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.html b/client/src/app/videos/video-list/shared/video-sort.component.html deleted file mode 100644 index 3bece0b22..000000000 --- a/client/src/app/videos/video-list/shared/video-sort.component.html +++ /dev/null | |||
@@ -1,5 +0,0 @@ | |||
1 | <select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()"> | ||
2 | <option *ngFor="let choice of choiceKeys" [value]="choice"> | ||
3 | {{ getStringChoice(choice) }} | ||
4 | </option> | ||
5 | </select> | ||
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.ts b/client/src/app/videos/video-list/shared/video-sort.component.ts deleted file mode 100644 index 8aa89d32b..000000000 --- a/client/src/app/videos/video-list/shared/video-sort.component.ts +++ /dev/null | |||
@@ -1,39 +0,0 @@ | |||
1 | import { Component, EventEmitter, Input, Output } from '@angular/core' | ||
2 | |||
3 | import { SortField } from '../../shared' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-video-sort', | ||
7 | templateUrl: './video-sort.component.html' | ||
8 | }) | ||
9 | |||
10 | export class VideoSortComponent { | ||
11 | @Output() sort = new EventEmitter<any>() | ||
12 | |||
13 | @Input() currentSort: SortField | ||
14 | |||
15 | sortChoices: { [ P in SortField ]: string } = { | ||
16 | 'name': 'Name - Asc', | ||
17 | '-name': 'Name - Desc', | ||
18 | 'duration': 'Duration - Asc', | ||
19 | '-duration': 'Duration - Desc', | ||
20 | 'createdAt': 'Created Date - Asc', | ||
21 | '-createdAt': 'Created Date - Desc', | ||
22 | 'views': 'Views - Asc', | ||
23 | '-views': 'Views - Desc', | ||
24 | 'likes': 'Likes - Asc', | ||
25 | '-likes': 'Likes - Desc' | ||
26 | } | ||
27 | |||
28 | get choiceKeys () { | ||
29 | return Object.keys(this.sortChoices) | ||
30 | } | ||
31 | |||
32 | getStringChoice (choiceKey: SortField) { | ||
33 | return this.sortChoices[choiceKey] | ||
34 | } | ||
35 | |||
36 | onSortChange () { | ||
37 | this.sort.emit(this.currentSort) | ||
38 | } | ||
39 | } | ||
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts deleted file mode 100644 index 784162679..000000000 --- a/client/src/app/videos/video-list/video-list.component.ts +++ /dev/null | |||
@@ -1,94 +0,0 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subscription } from 'rxjs/Subscription' | ||
4 | |||
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | |||
7 | import { VideoService } from '../shared' | ||
8 | import { Search, SearchField, SearchService } from '../../shared' | ||
9 | import { AbstractVideoList } from './shared' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-videos-list', | ||
13 | styleUrls: [ './shared/abstract-video-list.scss' ], | ||
14 | templateUrl: './shared/abstract-video-list.html' | ||
15 | }) | ||
16 | export class VideoListComponent extends AbstractVideoList implements OnInit, OnDestroy { | ||
17 | private search: Search | ||
18 | private subSearch: Subscription | ||
19 | |||
20 | constructor ( | ||
21 | protected router: Router, | ||
22 | protected route: ActivatedRoute, | ||
23 | protected notificationsService: NotificationsService, | ||
24 | private videoService: VideoService, | ||
25 | private searchService: SearchService | ||
26 | ) { | ||
27 | super() | ||
28 | } | ||
29 | |||
30 | ngOnInit () { | ||
31 | // Subscribe to route changes | ||
32 | this.subActivatedRoute = this.route.params.subscribe(routeParams => { | ||
33 | this.loadRouteParams(routeParams) | ||
34 | |||
35 | // Update the search service component | ||
36 | this.searchService.updateSearch.next(this.search) | ||
37 | this.getVideos() | ||
38 | }) | ||
39 | |||
40 | // Subscribe to search changes | ||
41 | this.subSearch = this.searchService.searchUpdated.subscribe(search => { | ||
42 | this.search = search | ||
43 | // Reset pagination | ||
44 | this.pagination.currentPage = 1 | ||
45 | |||
46 | this.navigateToNewParams() | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | ngOnDestroy () { | ||
51 | super.ngOnDestroy() | ||
52 | |||
53 | this.subSearch.unsubscribe() | ||
54 | } | ||
55 | |||
56 | getVideosObservable () { | ||
57 | let observable = null | ||
58 | if (this.search.value) { | ||
59 | observable = this.videoService.searchVideos(this.search, this.pagination, this.sort) | ||
60 | } else { | ||
61 | observable = this.videoService.getVideos(this.pagination, this.sort) | ||
62 | } | ||
63 | |||
64 | return observable | ||
65 | } | ||
66 | |||
67 | protected buildRouteParams () { | ||
68 | const params = super.buildRouteParams() | ||
69 | |||
70 | // Maybe there is a search | ||
71 | if (this.search.value) { | ||
72 | params['field'] = this.search.field | ||
73 | params['search'] = this.search.value | ||
74 | } | ||
75 | |||
76 | return params | ||
77 | } | ||
78 | |||
79 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { | ||
80 | super.loadRouteParams(routeParams) | ||
81 | |||
82 | if (routeParams['search'] !== undefined) { | ||
83 | this.search = { | ||
84 | value: routeParams['search'], | ||
85 | field: routeParams['field'] as SearchField | ||
86 | } | ||
87 | } else { | ||
88 | this.search = { | ||
89 | value: '', | ||
90 | field: 'name' | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | } | ||
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts new file mode 100644 index 000000000..6168fac95 --- /dev/null +++ b/client/src/app/videos/video-list/video-recently-added.component.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | ||
5 | import { SortField } from '../../shared/video/sort-field.type' | ||
6 | import { VideoService } from '../../shared/video/video.service' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-videos-recently-added', | ||
10 | styleUrls: [ '../../shared/video/abstract-video-list.scss' ], | ||
11 | templateUrl: '../../shared/video/abstract-video-list.html' | ||
12 | }) | ||
13 | export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit { | ||
14 | titlePage = 'Recently added' | ||
15 | currentRoute = '/videos/recently-added' | ||
16 | sort: SortField = '-createdAt' | ||
17 | |||
18 | constructor (protected router: Router, | ||
19 | protected route: ActivatedRoute, | ||
20 | protected notificationsService: NotificationsService, | ||
21 | private videoService: VideoService) { | ||
22 | super() | ||
23 | } | ||
24 | |||
25 | ngOnInit () { | ||
26 | super.ngOnInit() | ||
27 | } | ||
28 | |||
29 | getVideosObservable () { | ||
30 | return this.videoService.getVideos(this.pagination, this.sort) | ||
31 | } | ||
32 | } | ||
diff --git a/client/src/app/videos/video-list/video-search.component.ts b/client/src/app/videos/video-list/video-search.component.ts new file mode 100644 index 000000000..ba851d27e --- /dev/null +++ b/client/src/app/videos/video-list/video-search.component.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { AbstractVideoList } from 'app/shared/video/abstract-video-list' | ||
5 | import { Subscription } from 'rxjs/Subscription' | ||
6 | import { SortField } from '../../shared/video/sort-field.type' | ||
7 | import { VideoService } from '../../shared/video/video.service' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-videos-search', | ||
11 | styleUrls: [ '../../shared/video/abstract-video-list.scss' ], | ||
12 | templateUrl: '../../shared/video/abstract-video-list.html' | ||
13 | }) | ||
14 | export class VideoSearchComponent extends AbstractVideoList implements OnInit, OnDestroy { | ||
15 | titlePage = 'Search' | ||
16 | currentRoute = '/videos/search' | ||
17 | loadOnInit = false | ||
18 | |||
19 | private search = '' | ||
20 | private subActivatedRoute: Subscription | ||
21 | |||
22 | constructor (protected router: Router, | ||
23 | protected route: ActivatedRoute, | ||
24 | protected notificationsService: NotificationsService, | ||
25 | private videoService: VideoService) { | ||
26 | super() | ||
27 | } | ||
28 | |||
29 | ngOnInit () { | ||
30 | super.ngOnInit() | ||
31 | |||
32 | this.subActivatedRoute = this.route.queryParams.subscribe( | ||
33 | queryParams => { | ||
34 | this.search = queryParams['search'] | ||
35 | this.reloadVideos() | ||
36 | }, | ||
37 | |||
38 | err => this.notificationsService.error('Error', err.text) | ||
39 | ) | ||
40 | } | ||
41 | |||
42 | ngOnDestroy () { | ||
43 | if (this.subActivatedRoute) { | ||
44 | this.subActivatedRoute.unsubscribe() | ||
45 | } | ||
46 | } | ||
47 | |||
48 | getVideosObservable () { | ||
49 | return this.videoService.searchVideos(this.search, this.pagination, this.sort) | ||
50 | } | ||
51 | } | ||
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts new file mode 100644 index 000000000..e80fd7f2c --- /dev/null +++ b/client/src/app/videos/video-list/video-trending.component.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { AbstractVideoList } from 'app/shared/video/abstract-video-list' | ||
5 | import { SortField } from '../../shared/video/sort-field.type' | ||
6 | import { VideoService } from '../../shared/video/video.service' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-videos-trending', | ||
10 | styleUrls: [ '../../shared/video/abstract-video-list.scss' ], | ||
11 | templateUrl: '../../shared/video/abstract-video-list.html' | ||
12 | }) | ||
13 | export class VideoTrendingComponent extends AbstractVideoList implements OnInit { | ||
14 | titlePage = 'Trending' | ||
15 | currentRoute = '/videos/trending' | ||
16 | defaultSort: SortField = '-views' | ||
17 | |||
18 | constructor (protected router: Router, | ||
19 | protected route: ActivatedRoute, | ||
20 | protected notificationsService: NotificationsService, | ||
21 | private videoService: VideoService) { | ||
22 | super() | ||
23 | } | ||
24 | |||
25 | ngOnInit () { | ||
26 | super.ngOnInit() | ||
27 | } | ||
28 | |||
29 | getVideosObservable () { | ||
30 | return this.videoService.getVideos(this.pagination, this.sort) | ||
31 | } | ||
32 | } | ||
diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts index 3ca3e5486..6910421b7 100644 --- a/client/src/app/videos/videos-routing.module.ts +++ b/client/src/app/videos/videos-routing.module.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | |||
4 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
5 | 4 | import { VideoSearchComponent } from './video-list' | |
6 | import { VideoListComponent, MyVideosComponent } from './video-list' | 5 | import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' |
6 | import { VideoTrendingComponent } from './video-list/video-trending.component' | ||
7 | import { VideosComponent } from './videos.component' | 7 | import { VideosComponent } from './videos.component' |
8 | 8 | ||
9 | const videosRoutes: Routes = [ | 9 | const videosRoutes: Routes = [ |
@@ -13,20 +13,34 @@ const videosRoutes: Routes = [ | |||
13 | canActivateChild: [ MetaGuard ], | 13 | canActivateChild: [ MetaGuard ], |
14 | children: [ | 14 | children: [ |
15 | { | 15 | { |
16 | path: 'mine', | 16 | path: 'list', |
17 | component: MyVideosComponent, | 17 | pathMatch: 'full', |
18 | redirectTo: 'recently-added' | ||
19 | }, | ||
20 | { | ||
21 | path: 'trending', | ||
22 | component: VideoTrendingComponent, | ||
18 | data: { | 23 | data: { |
19 | meta: { | 24 | meta: { |
20 | title: 'My videos' | 25 | title: 'Trending videos' |
21 | } | 26 | } |
22 | } | 27 | } |
23 | }, | 28 | }, |
24 | { | 29 | { |
25 | path: 'list', | 30 | path: 'recently-added', |
26 | component: VideoListComponent, | 31 | component: VideoRecentlyAddedComponent, |
32 | data: { | ||
33 | meta: { | ||
34 | title: 'Recently added videos' | ||
35 | } | ||
36 | } | ||
37 | }, | ||
38 | { | ||
39 | path: 'search', | ||
40 | component: VideoSearchComponent, | ||
27 | data: { | 41 | data: { |
28 | meta: { | 42 | meta: { |
29 | title: 'Videos list' | 43 | title: 'Search videos' |
30 | } | 44 | } |
31 | } | 45 | } |
32 | }, | 46 | }, |
@@ -50,6 +64,7 @@ const videosRoutes: Routes = [ | |||
50 | }, | 64 | }, |
51 | { | 65 | { |
52 | path: ':uuid', | 66 | path: ':uuid', |
67 | pathMatch: 'full', | ||
53 | redirectTo: 'watch/:uuid' | 68 | redirectTo: 'watch/:uuid' |
54 | }, | 69 | }, |
55 | { | 70 | { |
diff --git a/client/src/app/videos/videos.module.ts b/client/src/app/videos/videos.module.ts index 4f3054c3a..4b14d1da8 100644 --- a/client/src/app/videos/videos.module.ts +++ b/client/src/app/videos/videos.module.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { SharedModule } from '../shared' | 2 | import { SharedModule } from '../shared' |
3 | import { VideoService } from './shared' | 3 | import { VideoSearchComponent } from './video-list' |
4 | import { MyVideosComponent, VideoListComponent, VideoMiniatureComponent, VideoSortComponent } from './video-list' | 4 | import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' |
5 | import { VideoTrendingComponent } from './video-list/video-trending.component' | ||
5 | import { VideosRoutingModule } from './videos-routing.module' | 6 | import { VideosRoutingModule } from './videos-routing.module' |
6 | import { VideosComponent } from './videos.component' | 7 | import { VideosComponent } from './videos.component' |
7 | 8 | ||
@@ -14,18 +15,15 @@ import { VideosComponent } from './videos.component' | |||
14 | declarations: [ | 15 | declarations: [ |
15 | VideosComponent, | 16 | VideosComponent, |
16 | 17 | ||
17 | VideoListComponent, | 18 | VideoTrendingComponent, |
18 | MyVideosComponent, | 19 | VideoRecentlyAddedComponent, |
19 | VideoMiniatureComponent, | 20 | VideoSearchComponent |
20 | VideoSortComponent | ||
21 | ], | 21 | ], |
22 | 22 | ||
23 | exports: [ | 23 | exports: [ |
24 | VideosComponent | 24 | VideosComponent |
25 | ], | 25 | ], |
26 | 26 | ||
27 | providers: [ | 27 | providers: [] |
28 | VideoService | ||
29 | ] | ||
30 | }) | 28 | }) |
31 | export class VideosModule { } | 29 | export class VideosModule { } |