aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/videos
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/videos')
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html10
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss5
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts2
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html51
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss93
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.module.ts2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/drag-drop.directive.ts30
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html13
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts7
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts95
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.scss10
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html23
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss45
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts13
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html38
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.scss93
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts6
-rw-r--r--client/src/app/videos/+video-edit/video-add.module.ts11
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.html22
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.scss24
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts9
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts7
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.ts12
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.model.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.service.ts13
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.html5
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.scss21
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.ts16
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.html82
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.scss10
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.ts12
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.html5
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.ts2
-rw-r--r--client/src/app/videos/+video-watch/video-watch-playlist.component.ts7
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html25
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss17
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts29
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts4
-rw-r--r--client/src/app/videos/recommendations/recommended-videos.component.html4
-rw-r--r--client/src/app/videos/recommendations/recommended-videos.component.scss22
-rw-r--r--client/src/app/videos/recommendations/recommended-videos.component.ts20
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-most-liked.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-overview.component.html55
-rw-r--r--client/src/app/videos/video-list/video-overview.component.ts67
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-user-subscriptions.component.ts4
50 files changed, 632 insertions, 439 deletions
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
index 19043eee6..6a9e31b5a 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
@@ -9,7 +9,7 @@
9 <div class="modal-body"> 9 <div class="modal-body">
10 <label i18n for="language">Language</label> 10 <label i18n for="language">Language</label>
11 <div class="peertube-select-container"> 11 <div class="peertube-select-container">
12 <select id="language" formControlName="language"> 12 <select id="language" formControlName="language" class="form-control">
13 <option></option> 13 <option></option>
14 <option *ngFor="let language of videoCaptionLanguages" [value]="language.id">{{ language.label }}</option> 14 <option *ngFor="let language of videoCaptionLanguages" [value]="language.id">{{ language.label }}</option>
15 </select> 15 </select>
@@ -23,6 +23,7 @@
23 <my-reactive-file 23 <my-reactive-file
24 formControlName="captionfile" inputName="captionfile" i18n-inputLabel inputLabel="Select the caption file" 24 formControlName="captionfile" inputName="captionfile" i18n-inputLabel inputLabel="Select the caption file"
25 [extensions]="videoCaptionExtensions" [maxFileSize]="videoCaptionMaxSize" [displayFilename]="true" 25 [extensions]="videoCaptionExtensions" [maxFileSize]="videoCaptionMaxSize" [displayFilename]="true"
26 i18n-ngbTooltip [ngbTooltip]="'(extensions: ' + videoCaptionExtensions.join(', ') + ')'"
26 ></my-reactive-file> 27 ></my-reactive-file>
27 </div> 28 </div>
28 29
@@ -32,9 +33,10 @@
32 </div> 33 </div>
33 34
34 <div class="modal-footer inputs"> 35 <div class="modal-footer inputs">
35 <span i18n class="action-button action-button-cancel" (click)="hide()"> 36 <input
36 Cancel 37 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
37 </span> 38 (click)="hide()" (key.enter)="hide()"
39 >
38 40
39 <input 41 <input
40 type="submit" i18n-value value="Add this caption" class="action-button-submit" 42 type="submit" i18n-value value="Add this caption" class="action-button-submit"
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss
index c6da1877e..b257a16a9 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss
@@ -7,6 +7,11 @@
7 7
8.caption-file { 8.caption-file {
9 margin-top: 20px; 9 margin-top: 20px;
10 width: max-content;
11
12 ::ng-deep .root {
13 width: max-content;
14 }
10} 15}
11 16
12.warning-replace-caption { 17.warning-replace-caption {
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
index 1a9bf5171..9856aac9e 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
@@ -56,7 +56,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
56 show () { 56 show () {
57 this.closingModal = false 57 this.closingModal = false
58 58
59 this.openedModal = this.modalService.open(this.modal, { keyboard: false }) 59 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
60 } 60 }
61 61
62 hide () { 62 hide () {
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index e40649d95..9a0e4f848 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -1,13 +1,15 @@
1<div class="video-edit" [formGroup]="form"> 1<div class="video-edit" [formGroup]="form">
2 <ngb-tabset class="root-tabset bootstrap"> 2 <div ngbNav #nav="ngbNav" class="nav-tabs">
3 3
4 <ngb-tab i18n-title title="Basic info"> 4 <ng-container ngbNavItem>
5 <ng-template ngbTabContent> 5 <a ngbNavLink i18n>Basic info</a>
6
7 <ng-template ngbNavContent>
6 <div class="row"> 8 <div class="row">
7 <div class="col-md-8"> 9 <div class="col-video-edit">
8 <div class="form-group"> 10 <div class="form-group">
9 <label i18n for="name">Title</label> 11 <label i18n for="name">Title</label>
10 <input type="text" id="name" formControlName="name" /> 12 <input type="text" id="name" class="form-control" formControlName="name" />
11 <div *ngIf="formErrors.name" class="form-error"> 13 <div *ngIf="formErrors.name" class="form-error">
12 {{ formErrors.name }} 14 {{ formErrors.name }}
13 </div> 15 </div>
@@ -29,7 +31,7 @@
29 <tag-input 31 <tag-input
30 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" 32 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
31 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag" 33 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag"
32 formControlName="tags" maxItems="5" modelAsStrings="true" 34 formControlName="tags" [maxItems]="5" [modelAsStrings]="true"
33 ></tag-input> 35 ></tag-input>
34 </div> 36 </div>
35 37
@@ -44,7 +46,7 @@
44 </ng-template> 46 </ng-template>
45 </my-help> 47 </my-help>
46 48
47 <my-markdown-textarea truncate="250" formControlName="description" markdownVideo="true"></my-markdown-textarea> 49 <my-markdown-textarea [truncate]="250" formControlName="description" [markdownVideo]="true"></my-markdown-textarea>
48 50
49 <div *ngIf="formErrors.description" class="form-error"> 51 <div *ngIf="formErrors.description" class="form-error">
50 {{ formErrors.description }} 52 {{ formErrors.description }}
@@ -52,11 +54,11 @@
52 </div> 54 </div>
53 </div> 55 </div>
54 56
55 <div class="col-md-4"> 57 <div class="col-video-edit">
56 <div class="form-group"> 58 <div class="form-group">
57 <label i18n>Channel</label> 59 <label i18n>Channel</label>
58 <div class="peertube-select-container"> 60 <div class="peertube-select-container">
59 <select formControlName="channelId"> 61 <select formControlName="channelId" class="form-control">
60 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> 62 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
61 </select> 63 </select>
62 </div> 64 </div>
@@ -65,7 +67,7 @@
65 <div class="form-group"> 67 <div class="form-group">
66 <label i18n for="category">Category</label> 68 <label i18n for="category">Category</label>
67 <div class="peertube-select-container"> 69 <div class="peertube-select-container">
68 <select id="category" formControlName="category"> 70 <select id="category" formControlName="category" class="form-control">
69 <option></option> 71 <option></option>
70 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> 72 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
71 </select> 73 </select>
@@ -79,7 +81,7 @@
79 <div class="form-group"> 81 <div class="form-group">
80 <label i18n for="licence">Licence</label> 82 <label i18n for="licence">Licence</label>
81 <div class="peertube-select-container"> 83 <div class="peertube-select-container">
82 <select id="licence" formControlName="licence"> 84 <select id="licence" formControlName="licence" class="form-control">
83 <option></option> 85 <option></option>
84 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> 86 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
85 </select> 87 </select>
@@ -93,7 +95,7 @@
93 <div class="form-group"> 95 <div class="form-group">
94 <label i18n for="language">Language</label> 96 <label i18n for="language">Language</label>
95 <div class="peertube-select-container"> 97 <div class="peertube-select-container">
96 <select id="language" formControlName="language"> 98 <select id="language" formControlName="language" class="form-control">
97 <option></option> 99 <option></option>
98 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> 100 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
99 </select> 101 </select>
@@ -107,7 +109,7 @@
107 <div class="form-group"> 109 <div class="form-group">
108 <label i18n for="privacy">Privacy</label> 110 <label i18n for="privacy">Privacy</label>
109 <div class="peertube-select-container"> 111 <div class="peertube-select-container">
110 <select id="privacy" formControlName="privacy"> 112 <select id="privacy" formControlName="privacy" class="form-control">
111 <option></option> 113 <option></option>
112 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 114 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
113 <option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option> 115 <option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
@@ -155,10 +157,12 @@
155 </div> 157 </div>
156 </div> 158 </div>
157 </ng-template> 159 </ng-template>
158 </ngb-tab> 160 </ng-container>
161
162 <ng-container ngbNavItem>
163 <a ngbNavLink i18n>Captions</a>
159 164
160 <ngb-tab i18n-title title="Captions"> 165 <ng-template ngbNavContent>
161 <ng-template ngbTabContent>
162 <div class="captions"> 166 <div class="captions">
163 167
164 <div class="captions-header"> 168 <div class="captions-header">
@@ -206,10 +210,12 @@
206 210
207 </div> 211 </div>
208 </ng-template> 212 </ng-template>
209 </ngb-tab> 213 </ng-container>
214
215 <ng-container ngbNavItem>
216 <a ngbNavLink i18n>Advanced settings</a>
210 217
211 <ngb-tab i18n-title title="Advanced settings"> 218 <ng-template ngbNavContent>
212 <ng-template ngbTabContent>
213 <div class="row advanced-settings"> 219 <div class="row advanced-settings">
214 <div class="col-md-12 col-xl-8"> 220 <div class="col-md-12 col-xl-8">
215 221
@@ -226,7 +232,7 @@
226 <label i18n for="support">Support</label> 232 <label i18n for="support">Support</label>
227 <my-help helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support you (membership platform...)."></my-help> 233 <my-help helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support you (membership platform...)."></my-help>
228 <my-markdown-textarea 234 <my-markdown-textarea
229 id="support" formControlName="support" textareaWidth="500px" [previewColumn]="true" markdownType="enhanced" 235 id="support" formControlName="support" markdownType="enhanced"
230 [classes]="{ 'input-error': formErrors['support'] }" 236 [classes]="{ 'input-error': formErrors['support'] }"
231 ></my-markdown-textarea> 237 ></my-markdown-textarea>
232 <div *ngIf="formErrors.support" class="form-error"> 238 <div *ngIf="formErrors.support" class="form-error">
@@ -262,10 +268,11 @@
262 </div> 268 </div>
263 </div> 269 </div>
264 </ng-template> 270 </ng-template>
265 </ngb-tab> 271 </ng-container>
266 272
267 </ngb-tabset> 273 </div>
268 274
275 <div [ngbNavOutlet]="nav"></div>
269</div> 276</div>
270 277
271<my-video-caption-add-modal 278<my-video-caption-add-modal
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 144914731..2f9067132 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,5 +1,16 @@
1@import '_variables'; 1// Bootstrap grid utilities require functions, variables and mixins
2@import '_mixins'; 2@import 'node_modules/bootstrap/scss/functions';
3@import 'node_modules/bootstrap/scss/variables';
4@import 'node_modules/bootstrap/scss/mixins';
5@import 'node_modules/bootstrap/scss/grid';
6
7@import 'variables';
8@import 'mixins';
9
10label {
11 font-weight: $font-regular;
12 font-size: 100%;
13}
3 14
4.peertube-select-container { 15.peertube-select-container {
5 @include peertube-select-container(auto); 16 @include peertube-select-container(auto);
@@ -19,6 +30,10 @@ my-peertube-checkbox {
19 margin-bottom: 1rem; 30 margin-bottom: 1rem;
20} 31}
21 32
33.nav-tabs {
34 margin-bottom: 15px;
35}
36
22.video-edit { 37.video-edit {
23 height: 100%; 38 height: 100%;
24 min-height: 300px; 39 min-height: 300px;
@@ -45,6 +60,7 @@ my-peertube-checkbox {
45 60
46 .captions-header { 61 .captions-header {
47 text-align: right; 62 text-align: right;
63 margin-bottom: 1rem;
48 64
49 .create-caption { 65 .create-caption {
50 @include create-button; 66 @include create-button;
@@ -59,6 +75,7 @@ my-peertube-checkbox {
59 a.caption-entry-label { 75 a.caption-entry-label {
60 @include disable-default-a-behaviour; 76 @include disable-default-a-behaviour;
61 77
78 flex-grow: 1;
62 color: #000; 79 color: #000;
63 80
64 &:hover { 81 &:hover {
@@ -144,69 +161,37 @@ p-calendar {
144 } 161 }
145} 162}
146 163
147::ng-deep { 164@include ng2-tags;
148 .root-tabset > .nav {
149 margin-bottom: 15px;
150 }
151 165
152 .ng2-tag-input { 166// columns for the video
153 border: none !important; 167.col-video-edit {
154 } 168 @include make-col-ready();
155 169
156 .ng2-tags-container { 170 @include media-breakpoint-up(md) {
157 display: flex; 171 @include make-col(7);
158 align-items: center;
159 border: 1px solid #C6C6C6;
160 border-radius: 3px;
161 padding: 5px !important;
162 height: max-content;
163 }
164 172
165 tag-input-form { 173 & + .col-video-edit {
166 input { 174 @include make-col(5);
167 height: 30px !important;
168 font-size: 12px !important;
169
170 background-color: var(--mainBackgroundColor) !important;
171 color: var(--mainForegroundColor) !important;
172 } 175 }
173 } 176 }
174 177
175 tag { 178 @include media-breakpoint-up(xl) {
176 background-color: $grey-background-color !important; 179 @include make-col(8);
177 color: #000 !important; 180
178 border-radius: 3px !important; 181 & + .col-video-edit {
179 font-size: 12px !important; 182 @include make-col(4);
180 height: 30px !important;
181 line-height: 30px !important;
182 margin: 0 5px 0 0 !important;
183 cursor: default !important;
184 padding: 0 8px 0 10px !important;
185
186 div {
187 height: 100% !important;
188 } 183 }
189 } 184 }
185}
190 186
191 delete-icon { 187:host-context(.expanded) {
192 cursor: pointer !important; 188 .col-video-edit {
193 height: auto !important; 189 @include media-breakpoint-up(md) {
194 vertical-align: middle !important; 190 @include make-col(8);
195 padding-left: 6px !important;
196
197 svg {
198 position: relative;
199 top: -1px;
200 height: auto !important;
201 vertical-align: middle !important;
202 191
203 path { 192 & + .col-video-edit {
204 fill: $grey-foreground-color !important; 193 @include make-col(4);
205 } 194 }
206 } 195 }
207
208 &:hover {
209 transform: none !important;
210 }
211 } 196 }
212} 197}
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 39b6daa93..1357d607c 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
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'
2import { TagInputModule } from 'ngx-chips' 2import { TagInputModule } from 'ngx-chips'
3import { SharedModule } from '../../../shared/' 3import { SharedModule } from '../../../shared/'
4import { VideoEditComponent } from './video-edit.component' 4import { VideoEditComponent } from './video-edit.component'
5import { CalendarModule } from 'primeng/components/calendar/calendar' 5import { CalendarModule } from 'primeng/calendar'
6import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 6import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
7 7
8@NgModule({ 8@NgModule({
diff --git a/client/src/app/videos/+video-edit/video-add-components/drag-drop.directive.ts b/client/src/app/videos/+video-edit/video-add-components/drag-drop.directive.ts
new file mode 100644
index 000000000..7b1a38c62
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-add-components/drag-drop.directive.ts
@@ -0,0 +1,30 @@
1import { Directive, Output, EventEmitter, HostBinding, HostListener } from '@angular/core'
2
3@Directive({
4 selector: '[dragDrop]'
5})
6export class DragDropDirective {
7 @Output() fileDropped = new EventEmitter<FileList>()
8
9 @HostBinding('class.dragover') dragover = false
10
11 @HostListener('dragover', ['$event']) onDragOver (e: Event) {
12 e.preventDefault()
13 e.stopPropagation()
14 this.dragover = true
15 }
16
17 @HostListener('dragleave', ['$event']) public onDragLeave (e: Event) {
18 e.preventDefault()
19 e.stopPropagation()
20 this.dragover = false
21 }
22
23 @HostListener('drop', ['$event']) public ondrop (e: DragEvent) {
24 e.preventDefault()
25 e.stopPropagation()
26 this.dragover = false
27 const files = e.dataTransfer.files
28 if (files.length > 0) this.fileDropped.emit(files)
29 }
30}
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
index c290fd4b1..c2ee3ad57 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -1,14 +1,13 @@
1<div *ngIf="!hasImportedVideo" class="upload-video-container"> 1<div *ngIf="!hasImportedVideo" class="upload-video-container" dragDrop (fileDropped)="setTorrentFile($event)">
2 <div class="first-step-block"> 2 <div class="first-step-block">
3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> 3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon>
4 4
5 <div class="button-file"> 5 <div class="button-file form-control" [ngbTooltip]="'(extensions: .torrent)'">
6 <span i18n>Select the torrent to import</span> 6 <span i18n>Select the torrent to import</span>
7 <input #torrentfileInput type="file" name="torrentfile" id="torrentfile" accept=".torrent" (change)="fileChange()" /> 7 <input #torrentfileInput type="file" name="torrentfile" id="torrentfile" accept=".torrent" (change)="fileChange()" />
8 </div> 8 </div>
9 <span class="button-file-extension">(.torrent)</span>
10 9
11 <div class="torrent-or-magnet" i18n>Or</div> 10 <div class="torrent-or-magnet" i18n-data-content data-content="OR"></div>
12 11
13 <div class="form-group form-group-magnet-uri"> 12 <div class="form-group form-group-magnet-uri">
14 <label i18n for="magnetUri">Paste magnet URI</label> 13 <label i18n for="magnetUri">Paste magnet URI</label>
@@ -21,13 +20,13 @@
21 </ng-template> 20 </ng-template>
22 </my-help> 21 </my-help>
23 22
24 <input type="text" id="magnetUri" [(ngModel)]="magnetUri" /> 23 <input type="text" id="magnetUri" [(ngModel)]="magnetUri" class="form-control" />
25 </div> 24 </div>
26 25
27 <div class="form-group"> 26 <div class="form-group">
28 <label i18n for="first-step-channel">Channel</label> 27 <label i18n for="first-step-channel">Channel</label>
29 <div class="peertube-select-container"> 28 <div class="peertube-select-container">
30 <select id="first-step-channel" [(ngModel)]="firstStepChannelId"> 29 <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control">
31 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> 30 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
32 </select> 31 </select>
33 </div> 32 </div>
@@ -36,7 +35,7 @@
36 <div class="form-group"> 35 <div class="form-group">
37 <label i18n for="first-step-privacy">Privacy</label> 36 <label i18n for="first-step-privacy">Privacy</label>
38 <div class="peertube-select-container"> 37 <div class="peertube-select-container">
39 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId"> 38 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control">
40 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 39 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
41 </select> 40 </select>
42 </div> 41 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
index 6d59ed834..3b46475b4 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
@@ -3,7 +3,11 @@
3 3
4.first-step-block { 4.first-step-block {
5 .torrent-or-magnet { 5 .torrent-or-magnet {
6 margin: 10px 0; 6 @include divider($color: var(--inputPlaceholderColor), $background: var(--submenuColor));
7
8 &[data-content] {
9 margin: 1.5rem 0;
10 }
7 } 11 }
8 12
9 .form-group-magnet-uri { 13 .form-group-magnet-uri {
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
index 74e1e755b..4d0b0b080 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -25,7 +25,7 @@ import { scrollToTop } from '@app/shared/misc/utils'
25export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 25export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
26 @Output() firstStepDone = new EventEmitter<string>() 26 @Output() firstStepDone = new EventEmitter<string>()
27 @Output() firstStepError = new EventEmitter<void>() 27 @Output() firstStepError = new EventEmitter<void>()
28 @ViewChild('torrentfileInput', { static: false }) torrentfileInput: ElementRef<HTMLInputElement> 28 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
29 29
30 magnetUri = '' 30 magnetUri = ''
31 31
@@ -72,6 +72,11 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
72 this.importVideo(torrentfile) 72 this.importVideo(torrentfile)
73 } 73 }
74 74
75 setTorrentFile (files: FileList) {
76 this.torrentfileInput.nativeElement.files = files
77 this.fileChange()
78 }
79
75 importVideo (torrentfile?: Blob) { 80 importVideo (torrentfile?: Blob) {
76 this.isImportingVideo = true 81 this.isImportingVideo = true
77 82
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
index 09d0b8272..9a26fe308 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
@@ -15,13 +15,13 @@
15 </ng-template> 15 </ng-template>
16 </my-help> 16 </my-help>
17 17
18 <input type="text" id="targetUrl" [(ngModel)]="targetUrl" /> 18 <input type="text" id="targetUrl" [(ngModel)]="targetUrl" class="form-control" />
19 </div> 19 </div>
20 20
21 <div class="form-group"> 21 <div class="form-group">
22 <label i18n for="first-step-channel">Channel</label> 22 <label i18n for="first-step-channel">Channel</label>
23 <div class="peertube-select-container"> 23 <div class="peertube-select-container">
24 <select id="first-step-channel" [(ngModel)]="firstStepChannelId"> 24 <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control">
25 <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>
26 </select> 26 </select>
27 </div> 27 </div>
@@ -30,7 +30,7 @@
30 <div class="form-group"> 30 <div class="form-group">
31 <label i18n for="first-step-privacy">Privacy</label> 31 <label i18n for="first-step-privacy">Privacy</label>
32 <div class="peertube-select-container"> 32 <div class="peertube-select-container">
33 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId"> 33 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control">
34 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 34 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
35 </select> 35 </select>
36 </div> 36 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
index a5578bebd..213c42333 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -11,7 +11,8 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
11import { FormValidatorService } from '@app/shared' 11import { FormValidatorService } from '@app/shared'
12import { VideoCaptionService } from '@app/shared/video-caption' 12import { VideoCaptionService } from '@app/shared/video-caption'
13import { VideoImportService } from '@app/shared/video-import' 13import { VideoImportService } from '@app/shared/video-import'
14import { scrollToTop } from '@app/shared/misc/utils' 14import { scrollToTop, getAbsoluteAPIUrl } from '@app/shared/misc/utils'
15import { switchMap, map } from 'rxjs/operators'
15 16
16@Component({ 17@Component({
17 selector: 'my-video-import-url', 18 selector: 'my-video-import-url',
@@ -76,31 +77,54 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
76 77
77 this.loadingBar.start() 78 this.loadingBar.start()
78 79
79 this.videoImportService.importVideoUrl(this.targetUrl, videoUpdate).subscribe( 80 this.videoImportService
80 res => { 81 .importVideoUrl(this.targetUrl, videoUpdate)
81 this.loadingBar.complete() 82 .pipe(
82 this.firstStepDone.emit(res.video.name) 83 switchMap(res => {
83 this.isImportingVideo = false 84 return this.videoCaptionService
84 this.hasImportedVideo = true 85 .listCaptions(res.video.id)
85 86 .pipe(
86 this.video = new VideoEdit(Object.assign(res.video, { 87 map(result => ({ video: res.video, videoCaptions: result.data }))
87 commentsEnabled: videoUpdate.commentsEnabled, 88 )
88 downloadEnabled: videoUpdate.downloadEnabled, 89 })
89 support: null, 90 )
90 thumbnailUrl: null, 91 .subscribe(
91 previewUrl: null 92 ({ video, videoCaptions }) => {
92 })) 93 this.loadingBar.complete()
93 94 this.firstStepDone.emit(video.name)
94 this.hydrateFormFromVideo() 95 this.isImportingVideo = false
95 }, 96 this.hasImportedVideo = true
96 97
97 err => { 98 const absoluteAPIUrl = getAbsoluteAPIUrl()
98 this.loadingBar.complete() 99
99 this.isImportingVideo = false 100 const thumbnailUrl = video.thumbnailPath
100 this.firstStepError.emit() 101 ? absoluteAPIUrl + video.thumbnailPath
101 this.notifier.error(err.message) 102 : null
102 } 103
103 ) 104 const previewUrl = video.previewPath
105 ? absoluteAPIUrl + video.previewPath
106 : null
107
108 this.video = new VideoEdit(Object.assign(video, {
109 commentsEnabled: videoUpdate.commentsEnabled,
110 downloadEnabled: videoUpdate.downloadEnabled,
111 support: null,
112 thumbnailUrl,
113 previewUrl
114 }))
115
116 this.videoCaptions = videoCaptions
117
118 this.hydrateFormFromVideo()
119 },
120
121 err => {
122 this.loadingBar.complete()
123 this.isImportingVideo = false
124 this.firstStepError.emit()
125 this.notifier.error(err.message)
126 }
127 )
104 } 128 }
105 129
106 updateSecondStep () { 130 updateSecondStep () {
@@ -133,5 +157,26 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
133 157
134 private hydrateFormFromVideo () { 158 private hydrateFormFromVideo () {
135 this.form.patchValue(this.video.toFormPatch()) 159 this.form.patchValue(this.video.toFormPatch())
160
161 const objects = [
162 {
163 url: 'thumbnailUrl',
164 name: 'thumbnailfile'
165 },
166 {
167 url: 'previewUrl',
168 name: 'previewfile'
169 }
170 ]
171
172 for (const obj of objects) {
173 fetch(this.video[obj.url])
174 .then(response => response.blob())
175 .then(data => {
176 this.form.patchValue({
177 [ obj.name ]: data
178 })
179 })
180 }
136 } 181 }
137} 182}
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.scss b/client/src/app/videos/+video-edit/video-add-components/video-send.scss
index 8769dd302..ebe14c59e 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-send.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-send.scss
@@ -41,14 +41,6 @@ $width-size: 190px;
41 } 41 }
42 42
43 .button-file { 43 .button-file {
44 @include peertube-button-file(auto); 44 @include peertube-button-file(max-content);
45
46 min-width: 190px;
47 }
48
49 .button-file-extension {
50 display: block;
51 font-size: 12px;
52 margin-top: 5px;
53 } 45 }
54} 46}
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
index 0f904affb..950e55a52 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
@@ -1,17 +1,16 @@
1<div *ngIf="!isUploadingVideo" class="upload-video-container"> 1<div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="setVideoFile($event)">
2 <div class="first-step-block"> 2 <div class="first-step-block">
3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> 3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon>
4 4
5 <div class="button-file"> 5 <div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'">
6 <span i18n>Select the file to upload</span> 6 <span i18n>Select the file to upload</span>
7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" /> 7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" autofocus />
8 </div> 8 </div>
9 <span class="button-file-extension">({{ videoExtensions }})</span>
10 9
11 <div class="form-group form-group-channel"> 10 <div class="form-group form-group-channel">
12 <label i18n for="first-step-channel">Channel</label> 11 <label i18n for="first-step-channel">Channel</label>
13 <div class="peertube-select-container"> 12 <div class="peertube-select-container">
14 <select id="first-step-channel" [(ngModel)]="firstStepChannelId"> 13 <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control">
15 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> 14 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
16 </select> 15 </select>
17 </div> 16 </div>
@@ -20,7 +19,7 @@
20 <div class="form-group"> 19 <div class="form-group">
21 <label i18n for="first-step-privacy">Privacy</label> 20 <label i18n for="first-step-privacy">Privacy</label>
22 <div class="peertube-select-container"> 21 <div class="peertube-select-container">
23 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId"> 22 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control">
24 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 23 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
25 <option i18n [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option> 24 <option i18n [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
26 </select> 25 </select>
@@ -51,10 +50,12 @@
51</div> 50</div>
52 51
53<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel"> 52<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
54 <p-progressBar 53 <div class="progress" i18n-title title="Total video quota">
55 [value]="videoUploadPercents" 54 <div class="progress-bar" role="progressbar" [style]="{ width: videoUploadPercents + '%' }" [attr.aria-valuenow]="videoUploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100">
56 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }" 55 <span *ngIf="videoUploadPercents === 100 && videoUploaded === false" i18n>Processing…</span>
57 ></p-progressBar> 56 <span *ngIf="videoUploadPercents !== 100 || videoUploaded">{{ videoUploadPercents }}%</span>
57 </div>
58 </div>
58 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" /> 59 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
59</div> 60</div>
60 61
@@ -68,7 +69,7 @@
68</div> 69</div>
69 70
70<!-- Hidden because we want to load the component --> 71<!-- Hidden because we want to load the component -->
71<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> 72<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form" class="mb-3">
72 <my-video-edit 73 <my-video-edit
73 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 74 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
74 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 75 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
index b5628e276..a4f87b0b8 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
@@ -2,7 +2,6 @@
2@import 'mixins'; 2@import 'mixins';
3 3
4.first-step-block { 4.first-step-block {
5
6 .form-group-channel { 5 .form-group-channel {
7 margin-bottom: 20px; 6 margin-bottom: 20px;
8 margin-top: 35px; 7 margin-top: 35px;
@@ -22,37 +21,21 @@
22 margin-top: 25px; 21 margin-top: 25px;
23 margin-bottom: 40px; 22 margin-bottom: 40px;
24 23
25 p-progressBar { 24 .progress {
25 @include progressbar;
26 flex-grow: 1; 26 flex-grow: 1;
27 27 height: 30px;
28 ::ng-deep .ui-progressbar { 28 font-size: 15px;
29 font-size: 15px !important; 29 background-color: rgba(11, 204, 41, 0.16);
30 height: 30px !important; 30
31 border-radius: 3px !important; 31 .progress-bar {
32 background-color: rgba(11, 204, 41, 0.16) !important; 32 background-color: $green;
33 33 line-height: 30px;
34 .ui-progressbar-value { 34 text-align: left;
35 background-color: #0BCC29 !important; 35 font-weight: $font-bold;
36 } 36
37 37 span {
38 .ui-progressbar-label { 38 margin-left: 18px;
39 text-align: left;
40 padding-left: 18px;
41 margin-top: 0 !important;
42 color: #fff !important;
43 line-height: 30px !important;
44 }
45 }
46
47 &.processing {
48 ::ng-deep .ui-progressbar-label {
49 // Same color as background to hide "100%"
50 color: rgba(11, 204, 41, 0.16) !important;
51
52 &::before {
53 content: 'Processing...';
54 color: #fff;
55 }
56 } 39 }
57 } 40 }
58 } 41 }
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
index aa87f9581..9ce3fbc6d 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
@@ -27,7 +27,7 @@ import { scrollToTop } from '@app/shared/misc/utils'
27export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 27export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
28 @Output() firstStepDone = new EventEmitter<string>() 28 @Output() firstStepDone = new EventEmitter<string>()
29 @Output() firstStepError = new EventEmitter<void>() 29 @Output() firstStepError = new EventEmitter<void>()
30 @ViewChild('videofileInput', { static: false }) videofileInput: ElementRef<HTMLInputElement> 30 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
31 31
32 // So that it can be accessed in the template 32 // So that it can be accessed in the template
33 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 33 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
@@ -70,7 +70,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
70 } 70 }
71 71
72 get videoExtensions () { 72 get videoExtensions () {
73 return this.serverConfig.video.file.extensions.join(',') 73 return this.serverConfig.video.file.extensions.join(', ')
74 } 74 }
75 75
76 ngOnInit () { 76 ngOnInit () {
@@ -108,6 +108,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
108 return this.videofileInput.nativeElement.files[0] 108 return this.videofileInput.nativeElement.files[0]
109 } 109 }
110 110
111 setVideoFile (files: FileList) {
112 this.videofileInput.nativeElement.files = files
113 this.fileChange()
114 }
115
111 getAudioUploadLabel () { 116 getAudioUploadLabel () {
112 const videofile = this.getVideoFile() 117 const videofile = this.getVideoFile()
113 if (!videofile) return this.i18n('Upload') 118 if (!videofile) return this.i18n('Upload')
@@ -122,9 +127,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
122 cancelUpload () { 127 cancelUpload () {
123 if (this.videoUploadObservable !== null) { 128 if (this.videoUploadObservable !== null) {
124 this.videoUploadObservable.unsubscribe() 129 this.videoUploadObservable.unsubscribe()
130
125 this.isUploadingVideo = false 131 this.isUploadingVideo = false
126 this.videoUploadPercents = 0 132 this.videoUploadPercents = 0
127 this.videoUploadObservable = null 133 this.videoUploadObservable = null
134
135 this.firstStepError.emit()
136
128 this.notifier.info(this.i18n('Upload cancelled')) 137 this.notifier.info(this.i18n('Upload cancelled'))
129 } 138 }
130 } 139 }
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 a99988600..79bfc6e5c 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -10,27 +10,37 @@
10 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> 10 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
11 </div> 11 </div>
12 12
13 <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }"> 13 <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
14 <ng-container ngbNavItem>
15 <a ngbNavLink>
16 <span i18n>Upload a file</span>
17 </a>
14 18
15 <ngb-tab> 19 <ng-template ngbNavContent>
16 <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template>
17 <ng-template ngbTabContent>
18 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)" (firstStepError)="onError()"></my-video-upload> 20 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)" (firstStepError)="onError()"></my-video-upload>
19 </ng-template> 21 </ng-template>
20 </ngb-tab> 22 </ng-container>
21 23
22 <ngb-tab *ngIf="isVideoImportHttpEnabled()"> 24 <ng-container ngbNavItem *ngIf="isVideoImportHttpEnabled()">
23 <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template> 25 <a ngbNavLink>
24 <ng-template ngbTabContent> 26 <span i18n>Import with URL</span>
27 </a>
28
29 <ng-template ngbNavContent>
25 <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)" (firstStepError)="onError()"></my-video-import-url> 30 <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)" (firstStepError)="onError()"></my-video-import-url>
26 </ng-template> 31 </ng-template>
27 </ngb-tab> 32 </ng-container>
33
34 <ng-container ngbNavItem *ngIf="isVideoImportTorrentEnabled()">
35 <a ngbNavLink>
36 <span i18n>Import with torrent</span>
37 </a>
28 38
29 <ngb-tab *ngIf="isVideoImportTorrentEnabled()"> 39 <ng-template ngbNavContent>
30 <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template>
31 <ng-template ngbTabContent>
32 <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent> 40 <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent>
33 </ng-template> 41 </ng-template>
34 </ngb-tab> 42 </ng-container>
35 </ngb-tabset> 43 </div>
44
45 <div [ngbNavOutlet]="nav"></div>
36</div> 46</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
index 7acab3744..1316e09e4 100644
--- a/client/src/app/videos/+video-edit/video-add.component.scss
+++ b/client/src/app/videos/+video-edit/video-add.component.scss
@@ -4,6 +4,7 @@
4$border-width: 3px; 4$border-width: 3px;
5$border-type: solid; 5$border-type: solid;
6$border-color: #EAEAEA; 6$border-color: #EAEAEA;
7$nav-link-height: 40px;
7 8
8.margin-content { 9.margin-content {
9 padding-top: 50px; 10 padding-top: 50px;
@@ -13,52 +14,76 @@ $border-color: #EAEAEA;
13 font-size: 15px; 14 font-size: 15px;
14} 15}
15 16
16::ng-deep .root-tabset.video-add-tabset { 17::ng-deep .video-add-nav {
17 margin-top: 50px; 18 border-bottom: $border-width $border-type $border-color;
19 margin: 50px 0 0 0 !important;
18 20
19 &.hide-nav > .nav { 21 &.hide-nav {
20 display: none !important; 22 display: none !important;
21 } 23 }
22 24
23 & > .nav { 25 a.nav-link {
24 border-bottom: $border-width $border-type $border-color; 26 @include disable-default-a-behaviour;
25 margin: 0 !important;
26 27
27 & > li { 28 margin-bottom: -$border-width;
28 margin-bottom: -$border-width; 29 height: $nav-link-height !important;
30 padding: 0 30px !important;
31 font-size: 15px;
32
33 &.active {
34 border: $border-width $border-type $border-color;
35 border-bottom: none;
36 background-color: var(--submenuColor) !important;
37
38 span {
39 border-bottom: 2px solid var(--mainColor);
40 font-weight: $font-bold;
41 }
29 } 42 }
43 }
44}
45
46::ng-deep .upload-video-container {
47 border: $border-width $border-type $border-color;
48 border-top: transparent;
30 49
31 a.nav-link { 50 background-color: var(--submenuColor);
32 @include disable-default-a-behaviour; 51 border-bottom-left-radius: 3px;
52 border-bottom-right-radius: 3px;
53 width: 100%;
54 min-height: 440px;
55 padding-bottom: 20px;
56 display: flex;
57 justify-content: center;
58 align-items: center;
33 59
34 height: 40px !important; 60 &.dragover {
35 padding: 0 30px !important; 61 border: 3px dashed var(--mainColor);
36 font-size: 15px; 62 }
63}
37 64
38 &.active { 65@mixin nav-scroll {
39 border: $border-width $border-type $border-color; 66 ::ng-deep .video-add-nav {
40 border-bottom: none; 67 height: #{$nav-link-height + $border-width * 2};
41 background-color: var(--submenuColor) !important; 68 overflow-x: auto;
69 white-space: nowrap;
70 flex-wrap: unset;
42 71
43 span { 72 /* Hide active tab style to not have a moving tab effect */
44 border-bottom: 2px solid var(--mainColor); 73 a.nav-link.active {
45 font-weight: $font-bold; 74 border: none;
46 } 75 background-color: var(--mainBackgroundColor) !important;
47 }
48 } 76 }
49 } 77 }
78}
79
80/* Make .video-add-nav tabs scrollable on small devices */
81@media screen and (max-width: $small-view) {
82 @include nav-scroll();
83}
50 84
51 .upload-video-container { 85@media screen and (max-width: #{$small-view + $menu-width}) {
52 border: $border-width $border-type $border-color; 86 :host-context(.main-col:not(.expanded)) {
53 border-top: none; 87 @include nav-scroll();
54
55 background-color: var(--submenuColor);
56 border-radius: 3px;
57 width: 100%;
58 min-height: 440px;
59 padding-bottom: 20px;
60 display: flex;
61 justify-content: center;
62 align-items: center;
63 } 88 }
64} 89}
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 401d8a08f..30ab08ea0 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -12,9 +12,9 @@ import { ServerConfig } from '@shared/models'
12 styleUrls: [ './video-add.component.scss' ] 12 styleUrls: [ './video-add.component.scss' ]
13}) 13})
14export class VideoAddComponent implements OnInit, CanComponentDeactivate { 14export class VideoAddComponent implements OnInit, CanComponentDeactivate {
15 @ViewChild('videoUpload', { static: false }) videoUpload: VideoUploadComponent 15 @ViewChild('videoUpload') videoUpload: VideoUploadComponent
16 @ViewChild('videoImportUrl', { static: false }) videoImportUrl: VideoImportUrlComponent 16 @ViewChild('videoImportUrl') videoImportUrl: VideoImportUrlComponent
17 @ViewChild('videoImportTorrent', { static: false }) videoImportTorrent: VideoImportTorrentComponent 17 @ViewChild('videoImportTorrent') videoImportTorrent: VideoImportTorrentComponent
18 18
19 secondStepType: 'upload' | 'import-url' | 'import-torrent' 19 secondStepType: 'upload' | 'import-url' | 'import-torrent'
20 videoName: string 20 videoName: string
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 870f7cb97..b8f5a9a47 100644
--- a/client/src/app/videos/+video-edit/video-add.module.ts
+++ b/client/src/app/videos/+video-edit/video-add.module.ts
@@ -3,27 +3,28 @@ import { SharedModule } from '../../shared'
3import { VideoEditModule } from './shared/video-edit.module' 3import { VideoEditModule } from './shared/video-edit.module'
4import { VideoAddRoutingModule } from './video-add-routing.module' 4import { VideoAddRoutingModule } from './video-add-routing.module'
5import { VideoAddComponent } from './video-add.component' 5import { VideoAddComponent } from './video-add.component'
6import { DragDropDirective } from './video-add-components/drag-drop.directive'
6import { CanDeactivateGuard } from '../../shared/guards/can-deactivate-guard.service' 7import { CanDeactivateGuard } from '../../shared/guards/can-deactivate-guard.service'
7import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component' 8import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component'
8import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component' 9import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component'
9import { VideoImportTorrentComponent } from '@app/videos/+video-edit/video-add-components/video-import-torrent.component' 10import { VideoImportTorrentComponent } from '@app/videos/+video-edit/video-add-components/video-import-torrent.component'
10import { ProgressBarModule } from 'primeng/progressbar'
11 11
12@NgModule({ 12@NgModule({
13 imports: [ 13 imports: [
14 VideoAddRoutingModule, 14 VideoAddRoutingModule,
15 VideoEditModule, 15 VideoEditModule,
16 SharedModule, 16 SharedModule
17 ProgressBarModule
18 ], 17 ],
19 declarations: [ 18 declarations: [
20 VideoAddComponent, 19 VideoAddComponent,
21 VideoUploadComponent, 20 VideoUploadComponent,
22 VideoImportUrlComponent, 21 VideoImportUrlComponent,
23 VideoImportTorrentComponent 22 VideoImportTorrentComponent,
23 DragDropDirective
24 ], 24 ],
25 exports: [ 25 exports: [
26 VideoAddComponent 26 VideoAddComponent,
27 DragDropDirective
27 ], 28 ],
28 providers: [ 29 providers: [
29 CanDeactivateGuard 30 CanDeactivateGuard
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
index 3a9977df6..9b43d91da 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
@@ -17,7 +17,7 @@
17 </div> 17 </div>
18 18
19 <div class="comment-buttons"> 19 <div class="comment-buttons">
20 <button *ngIf="isAddButtonDisplayed()" class="cancel-button" (click)="cancelCommentReply()" i18n> 20 <button *ngIf="isAddButtonDisplayed()" class="cancel-button" (click)="cancelCommentReply()" type="button" i18n>
21 Cancel 21 Cancel
22 </button> 22 </button>
23 <button *ngIf="isAddButtonDisplayed()" [ngClass]="{ disabled: !form.valid || addingComment }" i18n> 23 <button *ngIf="isAddButtonDisplayed()" [ngClass]="{ disabled: !form.valid || addingComment }" i18n>
@@ -29,15 +29,11 @@
29<ng-template #visitorModal let-modal> 29<ng-template #visitorModal let-modal>
30 <div class="modal-header"> 30 <div class="modal-header">
31 <h4 class="modal-title" id="modal-basic-title" i18n>You are one step away from commenting</h4> 31 <h4 class="modal-title" id="modal-basic-title" i18n>You are one step away from commenting</h4>
32 <button type="button" class="close" aria-label="Close" (click)="hideVisitorModal()"></button> 32 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideVisitorModal()"></my-global-icon>
33 </div> 33 </div>
34 <div class="modal-body"> 34 <div class="modal-body">
35 <span i18n> 35 <span i18n>
36 If you have an account on this instance, you can login: 36 You can comment using an account on any ActivityPub-compatible instance.
37 </span>
38 <span class="btn btn-sm mx-3" role="button" (click)="gotoLogin()" i18n>login to comment</span>
39 <span i18n>
40 Otherwise, you can comment using an account on any ActivityPub-compatible instance.
41 On most platforms, you can find the video by typing its URL in the search bar and then comment it 37 On most platforms, you can find the video by typing its URL in the search bar and then comment it
42 from within the software's interface. 38 from within the software's interface.
43 </span> 39 </span>
@@ -47,8 +43,14 @@
47 <my-remote-subscribe [interact]="true" [uri]="getUri()"></my-remote-subscribe> 43 <my-remote-subscribe [interact]="true" [uri]="getUri()"></my-remote-subscribe>
48 </div> 44 </div>
49 <div class="modal-footer inputs"> 45 <div class="modal-footer inputs">
50 <span i18n class="action-button action-button-cancel" role="button" (click)="hideVisitorModal()"> 46 <input
51 Cancel 47 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
52 </span> 48 (click)="hideVisitorModal()" (key.enter)="hideVisitorModal()"
49 >
50
51 <input
52 type="submit" i18n-value value="Login to comment" class="action-button-submit"
53 (click)="gotoLogin()"
54 >
53 </div> 55 </div>
54</ng-template> 56</ng-template>
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss b/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss
index c04727ba0..b3725ab94 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss
@@ -30,25 +30,33 @@ form {
30 } 30 }
31} 31}
32 32
33.cancel-button {
34 font-weight: $font-semibold;
35 display: inline-block;
36 padding: 0 10px 0 10px;
37 white-space: nowrap;
38 background: transparent;
39}
40
41.comment-buttons { 33.comment-buttons {
42 display: flex; 34 display: flex;
43 justify-content: flex-end; 35 justify-content: flex-end;
44 36
45 button { 37 button {
46 @include peertube-button; 38 @include peertube-button;
39 @include disable-outline;
40 @include disable-default-a-behaviour;
41
42 &:not(:last-child) {
43 margin-right: .5rem;
44 }
47 45
48 &:last-child { 46 &:last-child {
49 @include orange-button; 47 @include orange-button;
50 } 48 }
51 } 49 }
50
51 .cancel-button {
52 @include tertiary-button;
53
54 font-weight: $font-semibold;
55 display: inline-block;
56 padding: 0 10px 0 10px;
57 white-space: nowrap;
58 background: transparent;
59 }
52} 60}
53 61
54@media screen and (max-width: 600px) { 62@media screen and (max-width: 600px) {
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
index 1be96ad9e..e1a8f6260 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
@@ -11,7 +11,6 @@ import { VideoCommentService } from './video-comment.service'
11import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 11import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
12import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service' 12import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service'
13import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 13import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
14import { AuthService } from '@app/core/auth'
15 14
16@Component({ 15@Component({
17 selector: 'my-video-comment-add', 16 selector: 'my-video-comment-add',
@@ -25,7 +24,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
25 @Input() parentComments: VideoComment[] 24 @Input() parentComments: VideoComment[]
26 @Input() focusOnInit = false 25 @Input() focusOnInit = false
27 26
28 @Output() commentCreated = new EventEmitter<VideoCommentCreate>() 27 @Output() commentCreated = new EventEmitter<VideoComment>()
29 @Output() cancel = new EventEmitter() 28 @Output() cancel = new EventEmitter()
30 29
31 @ViewChild('visitorModal', { static: true }) visitorModal: NgbModal 30 @ViewChild('visitorModal', { static: true }) visitorModal: NgbModal
@@ -38,7 +37,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
38 private videoCommentValidatorsService: VideoCommentValidatorsService, 37 private videoCommentValidatorsService: VideoCommentValidatorsService,
39 private notifier: Notifier, 38 private notifier: Notifier,
40 private videoCommentService: VideoCommentService, 39 private videoCommentService: VideoCommentService,
41 private authService: AuthService,
42 private modalService: NgbModal, 40 private modalService: NgbModal,
43 private router: Router 41 private router: Router
44 ) { 42 ) {
@@ -57,7 +55,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
57 55
58 if (this.parentComment) { 56 if (this.parentComment) {
59 const mentions = this.parentComments 57 const mentions = this.parentComments
60 .filter(c => c.account.id !== this.user.account.id) // Don't add mention of ourselves 58 .filter(c => c.account && c.account.id !== this.user.account.id) // Don't add mention of ourselves
61 .map(c => '@' + c.by) 59 .map(c => '@' + c.by)
62 60
63 const mentionsSet = new Set(mentions) 61 const mentionsSet = new Set(mentions)
@@ -96,7 +94,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
96 this.addingComment = true 94 this.addingComment = true
97 95
98 const commentCreate: VideoCommentCreate = this.form.value 96 const commentCreate: VideoCommentCreate = this.form.value
99 let obs: Observable<any> 97 let obs: Observable<VideoComment>
100 98
101 if (this.parentComment) { 99 if (this.parentComment) {
102 obs = this.addCommentReply(commentCreate) 100 obs = this.addCommentReply(commentCreate)
@@ -139,6 +137,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
139 137
140 cancelCommentReply () { 138 cancelCommentReply () {
141 this.cancel.emit(null) 139 this.cancel.emit(null)
140 this.form.value['text'] = this.textareaElement.nativeElement.value = ''
142 } 141 }
143 142
144 private addCommentReply (commentCreate: VideoCommentCreate) { 143 private addCommentReply (commentCreate: VideoCommentCreate) {
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts b/client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts
new file mode 100644
index 000000000..1566d7369
--- /dev/null
+++ b/client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts
@@ -0,0 +1,7 @@
1import { VideoCommentThreadTree as VideoCommentThreadTreeServerModel } from '../../../../../../shared/models/videos/video-comment.model'
2import { VideoComment } from '@app/videos/+video-watch/comment/video-comment.model'
3
4export class VideoCommentThreadTree implements VideoCommentThreadTreeServerModel {
5 comment: VideoComment
6 children: VideoCommentThreadTree[]
7}
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
index 61f9335d1..868addd58 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
@@ -1,6 +1,5 @@
1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'
2import { User, UserRight } from '../../../../../../shared/models/users' 2import { User, UserRight } from '../../../../../../shared/models/users'
3import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
4import { AuthService } from '@app/core/auth' 3import { AuthService } from '@app/core/auth'
5import { AccountService } from '@app/shared/account/account.service' 4import { AccountService } from '@app/shared/account/account.service'
6import { Video } from '@app/shared/video/video.model' 5import { Video } from '@app/shared/video/video.model'
@@ -10,6 +9,7 @@ import { Account } from '@app/shared/account/account.model'
10import { Notifier } from '@app/core' 9import { Notifier } from '@app/core'
11import { UserService } from '@app/shared' 10import { UserService } from '@app/shared'
12import { Actor } from '@app/shared/actor/actor.model' 11import { Actor } from '@app/shared/actor/actor.model'
12import { VideoCommentThreadTree } from '@app/videos/+video-watch/comment/video-comment-thread-tree.model'
13 13
14@Component({ 14@Component({
15 selector: 'my-video-comment', 15 selector: 'my-video-comment',
@@ -98,6 +98,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
98 return this.comment.account && this.isUserLoggedIn() && 98 return this.comment.account && this.isUserLoggedIn() &&
99 ( 99 (
100 this.user.account.id === this.comment.account.id || 100 this.user.account.id === this.comment.account.id ||
101 this.user.account.id === this.video.account.id ||
101 this.user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) 102 this.user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT)
102 ) 103 )
103 } 104 }
@@ -125,7 +126,12 @@ export class VideoCommentComponent implements OnInit, OnChanges {
125 const html = await this.markdownService.textMarkdownToHTML(this.comment.text, true) 126 const html = await this.markdownService.textMarkdownToHTML(this.comment.text, true)
126 this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html) 127 this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html)
127 this.newParentComments = this.parentComments.concat([ this.comment ]) 128 this.newParentComments = this.parentComments.concat([ this.comment ])
128 this.commentAccount = new Account(this.comment.account) 129
129 this.getUserIfNeeded(this.commentAccount) 130 if (this.comment.account) {
131 this.commentAccount = new Account(this.comment.account)
132 this.getUserIfNeeded(this.commentAccount)
133 } else {
134 this.comment.account = null
135 }
130 } 136 }
131} 137}
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.model.ts b/client/src/app/videos/+video-watch/comment/video-comment.model.ts
index aaeb0ea9c..171fc4acc 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.model.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.model.ts
@@ -1,5 +1,5 @@
1import { Account as AccountInterface } from '../../../../../../shared/models/actors' 1import { Account as AccountInterface } from '../../../../../../shared/models/actors'
2import { VideoComment as VideoCommentServerModel } from '../../../../../../shared/models/videos/video-comment.model' 2import { VideoComment as VideoCommentServerModel, VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model'
3import { Actor } from '@app/shared/actor/actor.model' 3import { Actor } from '@app/shared/actor/actor.model'
4import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' 4import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
5 5
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
index a81e5236a..0b0715390 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
@@ -7,13 +7,14 @@ import { FeedFormat, ResultList } from '../../../../../../shared/models'
7import { 7import {
8 VideoComment as VideoCommentServerModel, 8 VideoComment as VideoCommentServerModel,
9 VideoCommentCreate, 9 VideoCommentCreate,
10 VideoCommentThreadTree 10 VideoCommentThreadTree as VideoCommentThreadTreeServerModel
11} from '../../../../../../shared/models/videos/video-comment.model' 11} from '../../../../../../shared/models/videos/video-comment.model'
12import { environment } from '../../../../environments/environment' 12import { environment } from '../../../../environments/environment'
13import { RestExtractor, RestService } from '../../../shared/rest' 13import { RestExtractor, RestService } from '../../../shared/rest'
14import { ComponentPaginationLight } from '../../../shared/rest/component-pagination.model' 14import { ComponentPaginationLight } from '../../../shared/rest/component-pagination.model'
15import { CommentSortField } from '../../../shared/video/sort-field.type' 15import { CommentSortField } from '../../../shared/video/sort-field.type'
16import { VideoComment } from './video-comment.model' 16import { VideoComment } from './video-comment.model'
17import { VideoCommentThreadTree } from '@app/videos/+video-watch/comment/video-comment-thread-tree.model'
17 18
18@Injectable() 19@Injectable()
19export class VideoCommentService { 20export class VideoCommentService {
@@ -76,9 +77,9 @@ export class VideoCommentService {
76 const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` 77 const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}`
77 78
78 return this.authHttp 79 return this.authHttp
79 .get(url) 80 .get<VideoCommentThreadTreeServerModel>(url)
80 .pipe( 81 .pipe(
81 map(tree => this.extractVideoCommentTree(tree as VideoCommentThreadTree)), 82 map(tree => this.extractVideoCommentTree(tree)),
82 catchError(err => this.restExtractor.handleError(err)) 83 catchError(err => this.restExtractor.handleError(err))
83 ) 84 )
84 } 85 }
@@ -138,12 +139,12 @@ export class VideoCommentService {
138 return { data: comments, total: totalComments } 139 return { data: comments, total: totalComments }
139 } 140 }
140 141
141 private extractVideoCommentTree (tree: VideoCommentThreadTree) { 142 private extractVideoCommentTree (tree: VideoCommentThreadTreeServerModel) {
142 if (!tree) return tree 143 if (!tree) return tree as VideoCommentThreadTree
143 144
144 tree.comment = new VideoComment(tree.comment) 145 tree.comment = new VideoComment(tree.comment)
145 tree.children.forEach(c => this.extractVideoCommentTree(c)) 146 tree.children.forEach(c => this.extractVideoCommentTree(c))
146 147
147 return tree 148 return tree as VideoCommentThreadTree
148 } 149 }
149} 150}
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html
index 2bf52ab86..a21042f09 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html
@@ -13,7 +13,7 @@
13 13
14 <div ngbDropdown class="d-inline-block ml-4"> 14 <div ngbDropdown class="d-inline-block ml-4">
15 <button class="btn btn-sm btn-outline-secondary" id="dropdownSortComments" ngbDropdownToggle i18n> 15 <button class="btn btn-sm btn-outline-secondary" id="dropdownSortComments" ngbDropdownToggle i18n>
16 Sort by 16 SORT BY
17 </button> 17 </button>
18 <div ngbDropdownMenu aria-labelledby="dropdownSortComments"> 18 <div ngbDropdownMenu aria-labelledby="dropdownSortComments">
19 <button (click)="handleSortChange('-createdAt')" ngbDropdownItem i18n>Most recent first (default)</button> 19 <button (click)="handleSortChange('-createdAt')" ngbDropdownItem i18n>Most recent first (default)</button>
@@ -38,7 +38,8 @@
38 (nearOfBottom)="onNearOfBottom()" 38 (nearOfBottom)="onNearOfBottom()"
39 [dataObservable]="onDataSubject.asObservable()" 39 [dataObservable]="onDataSubject.asObservable()"
40 > 40 >
41 <div #commentHighlightBlock id="highlighted-comment"> 41 <div>
42 <div class="anchor" #commentHighlightBlock id="highlighted-comment"></div>
42 <my-video-comment 43 <my-video-comment
43 *ngIf="highlightedThread" 44 *ngIf="highlightedThread"
44 [comment]="highlightedThread" 45 [comment]="highlightedThread"
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.scss b/client/src/app/videos/+video-watch/comment/video-comments.component.scss
index f95ff5aba..5ed1ac629 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.scss
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.scss
@@ -17,8 +17,20 @@
17 font-size: 13px; 17 font-size: 13px;
18} 18}
19 19
20.title-block .title-page { 20.title-block {
21 margin-right: 0; 21 .title-page {
22 margin-right: 0;
23 }
24
25 my-feed {
26 display: inline-block;
27 margin-left: 5px;
28 opacity: 0;
29 transition: ease-in .2s opacity;
30 }
31 &:hover my-feed {
32 opacity: 1;
33 }
22} 34}
23 35
24#dropdownSortComments { 36#dropdownSortComments {
@@ -28,11 +40,6 @@
28 transform: translateY(-7%); 40 transform: translateY(-7%);
29} 41}
30 42
31my-feed {
32 display: inline-block;
33 margin-left: 5px;
34}
35
36@media screen and (max-width: 600px) { 43@media screen and (max-width: 600px) {
37 .view-replies { 44 .view-replies {
38 margin-left: 46px; 45 margin-left: 46px;
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
index 974c61d6c..c6c28e3f7 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
@@ -1,8 +1,7 @@
1import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, Output, EventEmitter } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
2import { ActivatedRoute } from '@angular/router' 2import { ActivatedRoute } from '@angular/router'
3import { ConfirmService, Notifier } from '@app/core' 3import { ConfirmService, Notifier } from '@app/core'
4import { Subject, Subscription } from 'rxjs' 4import { Subject, Subscription } from 'rxjs'
5import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
6import { AuthService } from '../../../core/auth' 5import { AuthService } from '../../../core/auth'
7import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model' 6import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model'
8import { User } from '../../../shared/users' 7import { User } from '../../../shared/users'
@@ -13,6 +12,7 @@ import { VideoCommentService } from './video-comment.service'
13import { I18n } from '@ngx-translate/i18n-polyfill' 12import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Syndication } from '@app/shared/video/syndication.model' 13import { Syndication } from '@app/shared/video/syndication.model'
15import { HooksService } from '@app/core/plugins/hooks.service' 14import { HooksService } from '@app/core/plugins/hooks.service'
15import { VideoCommentThreadTree } from '@app/videos/+video-watch/comment/video-comment-thread-tree.model'
16 16
17@Component({ 17@Component({
18 selector: 'my-video-comments', 18 selector: 'my-video-comments',
@@ -20,7 +20,7 @@ import { HooksService } from '@app/core/plugins/hooks.service'
20 styleUrls: ['./video-comments.component.scss'] 20 styleUrls: ['./video-comments.component.scss']
21}) 21})
22export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { 22export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
23 @ViewChild('commentHighlightBlock', { static: false }) commentHighlightBlock: ElementRef 23 @ViewChild('commentHighlightBlock') commentHighlightBlock: ElementRef
24 @Input() video: VideoDetails 24 @Input() video: VideoDetails
25 @Input() user: User 25 @Input() user: User
26 26
@@ -96,6 +96,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
96 res => { 96 res => {
97 this.threadComments[commentId] = res 97 this.threadComments[commentId] = res
98 this.threadLoading[commentId] = false 98 this.threadLoading[commentId] = false
99 this.hooks.runAction('action:video-watch.video-thread-replies.loaded', 'video-watch', { data: res })
99 100
100 if (highlightThread) { 101 if (highlightThread) {
101 this.highlightedThread = new VideoComment(res.comment) 102 this.highlightedThread = new VideoComment(res.comment)
@@ -130,6 +131,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
130 this.componentPagination.totalItems = res.total 131 this.componentPagination.totalItems = res.total
131 132
132 this.onDataSubject.next(res.data) 133 this.onDataSubject.next(res.data)
134 this.hooks.runAction('action:video-watch.video-threads.loaded', 'video-watch', { data: this.componentPagination })
133 }, 135 },
134 136
135 err => this.notifier.error(err.message) 137 err => this.notifier.error(err.message)
@@ -167,7 +169,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
167 let message = 'Do you really want to delete this comment?' 169 let message = 'Do you really want to delete this comment?'
168 170
169 if (commentToDelete.isLocal) { 171 if (commentToDelete.isLocal) {
170 message += this.i18n(' The deletion will be sent to remote instances, so they remove the comment too.') 172 message += this.i18n(' The deletion will be sent to remote instances so they can reflect the change.')
171 } else { 173 } else {
172 message += this.i18n(' It is a remote comment, so the deletion will only be effective on your instance.') 174 message += this.i18n(' It is a remote comment, so the deletion will only be effective on your instance.')
173 } 175 }
@@ -181,7 +183,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
181 // Mark the comment as deleted 183 // Mark the comment as deleted
182 this.softDeleteComment(commentToDelete) 184 this.softDeleteComment(commentToDelete)
183 185
184 if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined 186 if (this.highlightedThread?.id === commentToDelete.id) this.highlightedThread = undefined
185 }, 187 },
186 188
187 err => this.notifier.error(err.message) 189 err => this.notifier.error(err.message)
@@ -193,9 +195,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
193 } 195 }
194 196
195 onNearOfBottom () { 197 onNearOfBottom () {
196 this.componentPagination.currentPage++
197
198 if (hasMoreItems(this.componentPagination)) { 198 if (hasMoreItems(this.componentPagination)) {
199 this.componentPagination.currentPage++
199 this.loadMoreThreads() 200 this.loadMoreThreads()
200 } 201 }
201 } 202 }
@@ -219,7 +220,6 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
219 this.componentPagination.totalItems = null 220 this.componentPagination.totalItems = null
220 221
221 this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid) 222 this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid)
222
223 this.loadMoreThreads() 223 this.loadMoreThreads()
224 } 224 }
225 } 225 }
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html
index 549a9f30e..5e6a2d518 100644
--- a/client/src/app/videos/+video-watch/modal/video-share.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-share.component.html
@@ -27,29 +27,33 @@
27 <div class="video"> 27 <div class="video">
28 <div class="title-page title-page-single" *ngIf="hasPlaylist()" i18n>Share the video</div> 28 <div class="title-page title-page-single" *ngIf="hasPlaylist()" i18n>Share the video</div>
29 29
30 <ngb-tabset class="root-tabset bootstrap" (tabChange)="onTabChange($event)"> 30 <div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeId">
31 31
32 <ngb-tab i18n-title title="URL" id="url"> 32 <ng-container ngbNavItem="url">
33 <ng-template ngbTabContent> 33 <a ngbNavLink i18n>URL</a>
34 34
35 <div class="tab-content"> 35 <ng-template ngbNavContent>
36 <div class="nav-content">
36 <my-input-readonly-copy [value]="getVideoUrl()"></my-input-readonly-copy> 37 <my-input-readonly-copy [value]="getVideoUrl()"></my-input-readonly-copy>
37 </div> 38 </div>
38
39 </ng-template> 39 </ng-template>
40 </ngb-tab> 40 </ng-container>
41
42 <ng-container ngbNavItem="qrcode">
43 <a ngbNavLink i18n>QR-Code</a>
41 44
42 <ngb-tab i18n-title title="QR-Code" id="qrcode"> 45 <ng-template ngbNavContent>
43 <ng-template ngbTabContent> 46 <div class="nav-content">
44 <div class="tab-content"> 47 <qrcode [qrdata]="getVideoUrl()" [size]="256" level="Q"></qrcode>
45 <qrcode [qrdata]="getVideoUrl()" size="256" level="Q"></qrcode>
46 </div> 48 </div>
47 </ng-template> 49 </ng-template>
48 </ngb-tab> 50 </ng-container>
51
52 <ng-container ngbNavItem="embed">
53 <a ngbNavLink i18n>Embed</a>
49 54
50 <ngb-tab i18n-title title="Embed" id="embed"> 55 <ng-template ngbNavContent>
51 <ng-template ngbTabContent> 56 <div class="nav-content">
52 <div class="tab-content">
53 <my-input-readonly-copy [value]="getVideoIframeCode()"></my-input-readonly-copy> 57 <my-input-readonly-copy [value]="getVideoIframeCode()"></my-input-readonly-copy>
54 58
55 <div i18n *ngIf="notSecure()" class="alert alert-warning"> 59 <div i18n *ngIf="notSecure()" class="alert alert-warning">
@@ -57,9 +61,11 @@
57 </div> 61 </div>
58 </div> 62 </div>
59 </ng-template> 63 </ng-template>
60 </ngb-tab> 64 </ng-container>
61 65
62 </ngb-tabset> 66 </div>
67
68 <div [ngbNavOutlet]="nav"></div>
63 69
64 <div class="filters"> 70 <div class="filters">
65 <div> 71 <div>
@@ -92,26 +98,6 @@
92 </div> 98 </div>
93 </div> 99 </div>
94 100
95 <div (click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed" role="button" class="advanced-filters-button"
96 [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic">
97
98 <ng-container *ngIf="isAdvancedCustomizationCollapsed">
99 <span class="glyphicon glyphicon-menu-down"></span>
100
101 <ng-container i18n>
102 More customization
103 </ng-container>
104 </ng-container>
105
106 <ng-container *ngIf="!isAdvancedCustomizationCollapsed">
107 <span class="glyphicon glyphicon-menu-up"></span>
108
109 <ng-container i18n>
110 Less customization
111 </ng-container>
112 </ng-container>
113 </div>
114
115 <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed"> 101 <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed">
116 <div> 102 <div>
117 <div class="form-group stop-at"> 103 <div class="form-group stop-at">
@@ -174,12 +160,28 @@
174 </div> 160 </div>
175 </ng-container> 161 </ng-container>
176 </div> 162 </div>
163
164 <div (click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed" role="button" class="advanced-filters-button"
165 [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic">
166
167 <ng-container *ngIf="isAdvancedCustomizationCollapsed">
168 <span class="glyphicon glyphicon-menu-down"></span>
169
170 <ng-container i18n>
171 More customization
172 </ng-container>
173 </ng-container>
174
175 <ng-container *ngIf="!isAdvancedCustomizationCollapsed">
176 <span class="glyphicon glyphicon-menu-up"></span>
177
178 <ng-container i18n>
179 Less customization
180 </ng-container>
181 </ng-container>
182 </div>
177 </div> 183 </div>
178 </div> 184 </div>
179 </div> 185 </div>
180 186
181 <div class="modal-footer inputs">
182 <span i18n class="action-button action-button-cancel" (click)="hide()">Close</span>
183 </div>
184
185</ng-template> 187</ng-template>
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.scss b/client/src/app/videos/+video-watch/modal/video-share.component.scss
index 8b5952da6..091d4dc3b 100644
--- a/client/src/app/videos/+video-watch/modal/video-share.component.scss
+++ b/client/src/app/videos/+video-watch/modal/video-share.component.scss
@@ -17,15 +17,11 @@ my-input-readonly-copy {
17 @include peertube-select-container(200px); 17 @include peertube-select-container(200px);
18} 18}
19 19
20.action-button-cancel {
21 margin-right: 0 !important;
22}
23
24.qr-code-group { 20.qr-code-group {
25 text-align: center; 21 text-align: center;
26} 22}
27 23
28.tab-content { 24.nav-content {
29 margin-top: 30px; 25 margin-top: 30px;
30 display: flex; 26 display: flex;
31 justify-content: center; 27 justify-content: center;
@@ -39,14 +35,12 @@ my-input-readonly-copy {
39 35
40.filters { 36.filters {
41 margin-top: 30px; 37 margin-top: 30px;
42 padding-top: 30px;
43 border-top: 1px solid $separator-border-color;
44 38
45 .advanced-filters-button { 39 .advanced-filters-button {
46 display: flex; 40 display: flex;
47 justify-content: center; 41 justify-content: center;
48 align-items: center; 42 align-items: center;
49 margin-top: 30px; 43 margin-top: 20px;
50 font-size: 16px; 44 font-size: 16px;
51 font-weight: $font-semibold; 45 font-weight: $font-semibold;
52 cursor: pointer; 46 cursor: pointer;
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts
index a2b38b3a0..3550556a0 100644
--- a/client/src/app/videos/+video-watch/modal/video-share.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-share.component.ts
@@ -1,9 +1,7 @@
1import { Component, ElementRef, Input, ViewChild } from '@angular/core' 1import { Component, ElementRef, Input, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
4import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' 3import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils'
5import { I18n } from '@ngx-translate/i18n-polyfill' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModal, NgbTabChangeEvent } from '@ng-bootstrap/ng-bootstrap'
7import { VideoCaption } from '@shared/models' 5import { VideoCaption } from '@shared/models'
8import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 6import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
9 7
@@ -37,7 +35,7 @@ export class VideoShareComponent {
37 @Input() videoCaptions: VideoCaption[] = [] 35 @Input() videoCaptions: VideoCaption[] = []
38 @Input() playlist: VideoPlaylist = null 36 @Input() playlist: VideoPlaylist = null
39 37
40 activeId: 'url' | 'qrcode' | 'embed' 38 activeId: 'url' | 'qrcode' | 'embed' = 'url'
41 customizations: Customizations 39 customizations: Customizations
42 isAdvancedCustomizationCollapsed = true 40 isAdvancedCustomizationCollapsed = true
43 includeVideoInPlaylist = false 41 includeVideoInPlaylist = false
@@ -74,7 +72,7 @@ export class VideoShareComponent {
74 controls: true 72 controls: true
75 } 73 }
76 74
77 this.modalService.open(this.modal) 75 this.modalService.open(this.modal, { centered: true })
78 } 76 }
79 77
80 getVideoIframeCode () { 78 getVideoIframeCode () {
@@ -103,10 +101,6 @@ export class VideoShareComponent {
103 return window.location.protocol === 'http:' 101 return window.location.protocol === 'http:'
104 } 102 }
105 103
106 onTabChange (event: NgbTabChangeEvent) {
107 this.activeId = event.nextId as any
108 }
109
110 isInEmbedTab () { 104 isInEmbedTab () {
111 return this.activeId === 'embed' 105 return this.activeId === 'embed'
112 } 106 }
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.html b/client/src/app/videos/+video-watch/modal/video-support.component.html
index 608a4632b..935656d23 100644
--- a/client/src/app/videos/+video-watch/modal/video-support.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-support.component.html
@@ -7,6 +7,9 @@
7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> 7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div>
8 8
9 <div class="modal-footer inputs"> 9 <div class="modal-footer inputs">
10 <span i18n class="action-button action-button-cancel" (click)="hide()">Maybe later</span> 10 <input
11 type="button" role="button" i18n-value value="Maybe later" class="action-button action-button-cancel"
12 (click)="hide()" (key.enter)="hide()"
13 >
11 </div> 14 </div>
12</ng-template> 15</ng-template>
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts
index b56a51fbf..0058172f2 100644
--- a/client/src/app/videos/+video-watch/modal/video-support.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-support.component.ts
@@ -21,7 +21,7 @@ export class VideoSupportComponent {
21 ) { } 21 ) { }
22 22
23 show () { 23 show () {
24 this.modalService.open(this.modal) 24 this.modalService.open(this.modal, { centered: true })
25 25
26 this.markdownService.enhancedMarkdownToHTML(this.video.support) 26 this.markdownService.enhancedMarkdownToHTML(this.video.support)
27 .then(r => this.videoHTMLSupport = r) 27 .then(r => this.videoHTMLSupport = r)
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
index c5ed36000..827c34d41 100644
--- a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
@@ -9,6 +9,7 @@ import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.
9import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' 9import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
10import { peertubeLocalStorage, peertubeSessionStorage } from '@app/shared/misc/peertube-web-storage' 10import { peertubeLocalStorage, peertubeSessionStorage } from '@app/shared/misc/peertube-web-storage'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { SessionStorageService, LocalStorageService } from '@app/shared/misc/storage.service'
12 13
13@Component({ 14@Component({
14 selector: 'my-video-watch-playlist', 15 selector: 'my-video-watch-playlist',
@@ -42,16 +43,18 @@ export class VideoWatchPlaylistComponent {
42 private notifier: Notifier, 43 private notifier: Notifier,
43 private i18n: I18n, 44 private i18n: I18n,
44 private videoPlaylist: VideoPlaylistService, 45 private videoPlaylist: VideoPlaylistService,
46 private localStorageService: LocalStorageService,
47 private sessionStorageService: SessionStorageService,
45 private router: Router 48 private router: Router
46 ) { 49 ) {
47 // defaults to true 50 // defaults to true
48 this.autoPlayNextVideoPlaylist = this.auth.isLoggedIn() 51 this.autoPlayNextVideoPlaylist = this.auth.isLoggedIn()
49 ? this.auth.getUser().autoPlayNextVideoPlaylist 52 ? this.auth.getUser().autoPlayNextVideoPlaylist
50 : peertubeLocalStorage.getItem(VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) !== 'false' 53 : this.localStorageService.getItem(VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) !== 'false'
51 this.setAutoPlayNextVideoPlaylistSwitchText() 54 this.setAutoPlayNextVideoPlaylistSwitchText()
52 55
53 // defaults to false 56 // defaults to false
54 this.loopPlaylist = peertubeSessionStorage.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) === 'true' 57 this.loopPlaylist = this.sessionStorageService.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) === 'true'
55 this.setLoopPlaylistSwitchText() 58 this.setLoopPlaylistSwitchText()
56 } 59 }
57 60
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 bc3a3ffdd..0244860dd 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -84,12 +84,12 @@
84 placement="bottom auto" 84 placement="bottom auto"
85 > 85 >
86 <my-global-icon iconName="support"></my-global-icon> 86 <my-global-icon iconName="support"></my-global-icon>
87 <span class="icon-text" i18n>Support</span> 87 <span class="icon-text" i18n>SUPPORT</span>
88 </div> 88 </div>
89 89
90 <div (click)="showShareModal()" class="action-button" role="button"> 90 <div (click)="showShareModal()" class="action-button" role="button">
91 <my-global-icon iconName="share"></my-global-icon> 91 <my-global-icon iconName="share"></my-global-icon>
92 <span class="icon-text" i18n>Share</span> 92 <span class="icon-text" i18n>SHARE</span>
93 </div> 93 </div>
94 94
95 <div 95 <div
@@ -100,7 +100,7 @@
100 > 100 >
101 <div class="action-button action-button-save" ngbDropdownToggle role="button"> 101 <div class="action-button action-button-save" ngbDropdownToggle role="button">
102 <my-global-icon iconName="playlist-add"></my-global-icon> 102 <my-global-icon iconName="playlist-add"></my-global-icon>
103 <span class="icon-text" i18n>Save</span> 103 <span class="icon-text" i18n>SAVE</span>
104 </div> 104 </div>
105 105
106 <div ngbDropdownMenu> 106 <div ngbDropdownMenu>
@@ -188,6 +188,11 @@
188 <span class="video-attribute-value">{{ video.privacy.label }}</span> 188 <span class="video-attribute-value">{{ video.privacy.label }}</span>
189 </div> 189 </div>
190 190
191 <div *ngIf="video.isLocal === false" class="video-attribute">
192 <span i18n class="video-attribute-label">Origin instance</span>
193 <a class="video-attribute-value" target="_blank" rel="noopener noreferrer" [href]="video.originInstanceUrl">{{ video.originInstanceHost }}</a>
194 </div>
195
191 <div *ngIf="!!video.originallyPublishedAt" class="video-attribute"> 196 <div *ngIf="!!video.originallyPublishedAt" class="video-attribute">
192 <span i18n class="video-attribute-label">Originally published</span> 197 <span i18n class="video-attribute-label">Originally published</span>
193 <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span> 198 <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
@@ -252,14 +257,16 @@
252 257
253 <div class="row privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false"> 258 <div class="row privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">
254 <div class="privacy-concerns-text"> 259 <div class="privacy-concerns-text">
255 <strong i18n>Friendly Reminder: </strong> 260 <span class="mr-2">
256 <ng-container i18n> 261 <strong i18n>Friendly Reminder: </strong>
257 the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers. 262 <ng-container i18n>
258 </ng-container> 263 the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers.
259 <a i18n i18n-title title="Get more information" target="_blank" rel="noopener noreferrer" href="/about/peertube">More information</a> 264 </ng-container>
265 </span>
266 <a i18n i18n-title title="Get more information" target="_blank" rel="noopener noreferrer" href="/about/peertube#privacy">More information</a>
260 </div> 267 </div>
261 268
262 <div i18n class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()"> 269 <div i18n class="privacy-concerns-button privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
263 OK 270 OK
264 </div> 271 </div>
265 </div> 272 </div>
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 c92f773e4..977312a83 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -53,7 +53,6 @@ $video-info-margin-left: 44px;
53 background-color: #000; 53 background-color: #000;
54 display: flex; 54 display: flex;
55 justify-content: center; 55 justify-content: center;
56 margin: 0 -15px;
57 56
58 #videojs-wrapper { 57 #videojs-wrapper {
59 display: flex; 58 display: flex;
@@ -443,6 +442,7 @@ my-video-comments {
443 442
444// If the view is not expanded, take into account the menu 443// If the view is not expanded, take into account the menu
445.privacy-concerns { 444.privacy-concerns {
445 z-index: z(dropdown) + 1;
446 width: calc(100% - #{$menu-width}); 446 width: calc(100% - #{$menu-width});
447} 447}
448 448
@@ -462,13 +462,14 @@ my-video-comments {
462.privacy-concerns { 462.privacy-concerns {
463 position: fixed; 463 position: fixed;
464 bottom: 0; 464 bottom: 0;
465 z-index: z(privacymsg);
465 466
466 padding: 5px 15px; 467 padding: 5px 15px;
467 468
468 display: flex; 469 display: flex;
469 flex-wrap: nowrap; 470 flex-wrap: nowrap;
470 align-items: center; 471 align-items: center;
471 justify-content: flex-start; 472 justify-content: space-between;
472 background-color: rgba(0, 0, 0, 0.9); 473 background-color: rgba(0, 0, 0, 0.9);
473 color: #fff; 474 color: #fff;
474 475
@@ -487,11 +488,11 @@ my-video-comments {
487 } 488 }
488 } 489 }
489 490
490 .privacy-concerns-okay { 491 .privacy-concerns-button {
491 background-color: var(--mainColor);
492 padding: 5px 8px 5px 7px; 492 padding: 5px 8px 5px 7px;
493 margin-left: auto; 493 margin-left: auto;
494 border-radius: 3px; 494 border-radius: 3px;
495 white-space: nowrap;
495 cursor: pointer; 496 cursor: pointer;
496 transition: background-color 0.3s; 497 transition: background-color 0.3s;
497 font-weight: $font-semibold; 498 font-weight: $font-semibold;
@@ -500,6 +501,11 @@ my-video-comments {
500 background-color: #000; 501 background-color: #000;
501 } 502 }
502 } 503 }
504
505 .privacy-concerns-okay {
506 background-color: var(--mainColor);
507 margin-left: 10px;
508 }
503} 509}
504 510
505@media screen and (max-width: 1600px) { 511@media screen and (max-width: 1600px) {
@@ -545,7 +551,8 @@ my-video-comments {
545 551
546@media screen and (max-width: 600px) { 552@media screen and (max-width: 600px) {
547 .video-bottom { 553 .video-bottom {
548 margin: 20px 0 0 0 !important; 554 margin-top: 20px !important;
555 margin-bottom: 20px !important;
549 556
550 .video-info { 557 .video-info {
551 padding: 0; 558 padding: 0;
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 e09e44809..51e484275 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -2,7 +2,7 @@ import { catchError } from 'rxjs/operators'
2import { ChangeDetectorRef, Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' 2import { ChangeDetectorRef, Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { RedirectService } from '@app/core/routing/redirect.service' 4import { RedirectService } from '@app/core/routing/redirect.service'
5import { peertubeLocalStorage, peertubeSessionStorage } from '@app/shared/misc/peertube-web-storage' 5import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { AuthUser, Notifier, ServerService } from '@app/core' 8import { AuthUser, Notifier, ServerService } from '@app/core'
@@ -10,7 +10,7 @@ import { forkJoin, Observable, Subscription } from 'rxjs'
10import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
11import { ServerConfig, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' 11import { ServerConfig, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
12import { AuthService, ConfirmService } from '../../core' 12import { AuthService, ConfirmService } from '../../core'
13import { RestExtractor } from '../../shared' 13import { RestExtractor, UserService } from '../../shared'
14import { VideoDetails } from '../../shared/video/video-details.model' 14import { VideoDetails } from '../../shared/video/video-details.model'
15import { VideoService } from '../../shared/video/video.service' 15import { VideoService } from '../../shared/video/video.service'
16import { VideoShareComponent } from './modal/video-share.component' 16import { VideoShareComponent } from './modal/video-share.component'
@@ -35,7 +35,6 @@ import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watc
35import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage' 35import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage'
36import { HooksService } from '@app/core/plugins/hooks.service' 36import { HooksService } from '@app/core/plugins/hooks.service'
37import { PlatformLocation } from '@angular/common' 37import { PlatformLocation } from '@angular/common'
38import { RecommendedVideosComponent } from '../recommendations/recommended-videos.component'
39import { scrollToTop, isXPercentInViewport } from '@app/shared/misc/utils' 38import { scrollToTop, isXPercentInViewport } from '@app/shared/misc/utils'
40 39
41@Component({ 40@Component({
@@ -47,9 +46,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
47 private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern' 46 private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
48 47
49 @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent 48 @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent
50 @ViewChild('videoShareModal', { static: false }) videoShareModal: VideoShareComponent 49 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
51 @ViewChild('videoSupportModal', { static: false }) videoSupportModal: VideoSupportComponent 50 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
52 @ViewChild('subscribeButton', { static: false }) subscribeButton: SubscribeButtonComponent 51 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
53 52
54 player: any 53 player: any
55 playerElement: HTMLVideoElement 54 playerElement: HTMLVideoElement
@@ -95,6 +94,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
95 private confirmService: ConfirmService, 94 private confirmService: ConfirmService,
96 private metaService: MetaService, 95 private metaService: MetaService,
97 private authService: AuthService, 96 private authService: AuthService,
97 private userService: UserService,
98 private serverService: ServerService, 98 private serverService: ServerService,
99 private restExtractor: RestExtractor, 99 private restExtractor: RestExtractor,
100 private notifier: Notifier, 100 private notifier: Notifier,
@@ -118,6 +118,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
118 return this.authService.getUser() 118 return this.authService.getUser()
119 } 119 }
120 120
121 get anonymousUser () {
122 return this.userService.getAnonymousUser()
123 }
124
121 async ngOnInit () { 125 async ngOnInit () {
122 this.serverConfig = this.serverService.getTmpConfig() 126 this.serverConfig = this.serverService.getTmpConfig()
123 127
@@ -266,6 +270,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
266 this.redirectService.redirectToHomepage() 270 this.redirectService.redirectToHomepage()
267 } 271 }
268 272
273 declinedPrivacyConcern () {
274 peertubeLocalStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'false')
275 this.hasAlreadyAcceptedPrivacyConcern = false
276 }
277
269 acceptedPrivacyConcern () { 278 acceptedPrivacyConcern () {
270 peertubeLocalStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true') 279 peertubeLocalStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true')
271 this.hasAlreadyAcceptedPrivacyConcern = true 280 this.hasAlreadyAcceptedPrivacyConcern = true
@@ -290,7 +299,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
290 isAutoPlayEnabled () { 299 isAutoPlayEnabled () {
291 return ( 300 return (
292 (this.user && this.user.autoPlayNextVideo) || 301 (this.user && this.user.autoPlayNextVideo) ||
293 peertubeSessionStorage.getItem(RecommendedVideosComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' 302 this.anonymousUser.autoPlayNextVideo
294 ) 303 )
295 } 304 }
296 305
@@ -302,7 +311,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
302 isPlaylistAutoPlayEnabled () { 311 isPlaylistAutoPlayEnabled () {
303 return ( 312 return (
304 (this.user && this.user.autoPlayNextVideoPlaylist) || 313 (this.user && this.user.autoPlayNextVideoPlaylist) ||
305 peertubeSessionStorage.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) === 'true' 314 this.anonymousUser.autoPlayNextVideoPlaylist
306 ) 315 )
307 } 316 }
308 317
@@ -532,7 +541,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
532 } 541 }
533 542
534 private autoplayNext () { 543 private autoplayNext () {
535 if (this.nextVideoUuid) { 544 if (this.playlist) {
545 this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
546 } else if (this.nextVideoUuid) {
536 this.router.navigate([ '/videos/watch', this.nextVideoUuid ]) 547 this.router.navigate([ '/videos/watch', this.nextVideoUuid ])
537 } 548 }
538 } 549 }
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 5fa50ecbb..9b445269d 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -12,7 +12,6 @@ import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
12import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' 12import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
13import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' 13import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
14import { QRCodeModule } from 'angularx-qrcode' 14import { QRCodeModule } from 'angularx-qrcode'
15import { InputSwitchModule } from 'primeng/inputswitch'
16import { TimestampRouteTransformerDirective } from '@app/shared/angular/timestamp-route-transformer.directive' 15import { TimestampRouteTransformerDirective } from '@app/shared/angular/timestamp-route-transformer.directive'
17 16
18@NgModule({ 17@NgModule({
@@ -21,8 +20,7 @@ import { TimestampRouteTransformerDirective } from '@app/shared/angular/timestam
21 SharedModule, 20 SharedModule,
22 NgbTooltipModule, 21 NgbTooltipModule,
23 QRCodeModule, 22 QRCodeModule,
24 RecommendationsModule, 23 RecommendationsModule
25 InputSwitchModule
26 ], 24 ],
27 25
28 declarations: [ 26 declarations: [
diff --git a/client/src/app/videos/recommendations/recommended-videos.component.html b/client/src/app/videos/recommendations/recommended-videos.component.html
index 476eca071..74f9ed2a5 100644
--- a/client/src/app/videos/recommendations/recommended-videos.component.html
+++ b/client/src/app/videos/recommendations/recommended-videos.component.html
@@ -7,8 +7,8 @@
7 <div *ngIf="!playlist" class="title-page-autoplay" 7 <div *ngIf="!playlist" class="title-page-autoplay"
8 [ngbTooltip]="autoPlayNextVideoTooltip" placement="bottom-right auto" 8 [ngbTooltip]="autoPlayNextVideoTooltip" placement="bottom-right auto"
9 > 9 >
10 <span i18n>Autoplay</span> 10 <span i18n>AUTOPLAY</span>
11 <p-inputSwitch [(ngModel)]="autoPlayNextVideo" (ngModelChange)="switchAutoPlayNextVideo()"></p-inputSwitch> 11 <p-inputSwitch class="small" [(ngModel)]="autoPlayNextVideo" (ngModelChange)="switchAutoPlayNextVideo()"></p-inputSwitch>
12 </div> 12 </div>
13 </div> 13 </div>
14 14
diff --git a/client/src/app/videos/recommendations/recommended-videos.component.scss b/client/src/app/videos/recommendations/recommended-videos.component.scss
index 1ab0c47ff..cde62f87f 100644
--- a/client/src/app/videos/recommendations/recommended-videos.component.scss
+++ b/client/src/app/videos/recommendations/recommended-videos.component.scss
@@ -25,25 +25,3 @@
25 font-weight: 600; 25 font-weight: 600;
26 } 26 }
27} 27}
28
29/* p-inputSwitch styles to reduce the switch size */
30
31::ng-deep {
32 p-inputswitch {
33 height: 20px;
34 }
35
36 .ui-inputswitch {
37 width: 2.5em !important;
38 height: 1.45em !important;
39
40 .ui-inputswitch-slider::before {
41 height: 1em !important;
42 width: 1em !important;
43 }
44 }
45
46 .ui-inputswitch-checked .ui-inputswitch-slider::before {
47 transform: translateX(1em) !important;
48 }
49}
diff --git a/client/src/app/videos/recommendations/recommended-videos.component.ts b/client/src/app/videos/recommendations/recommended-videos.component.ts
index ada6d3433..d4b4c929b 100644
--- a/client/src/app/videos/recommendations/recommended-videos.component.ts
+++ b/client/src/app/videos/recommendations/recommended-videos.component.ts
@@ -7,8 +7,8 @@ import { RecommendedVideosStore } from '@app/videos/recommendations/recommended-
7import { User } from '@app/shared' 7import { User } from '@app/shared'
8import { AuthService, Notifier } from '@app/core' 8import { AuthService, Notifier } from '@app/core'
9import { UserService } from '@app/shared/users/user.service' 9import { UserService } from '@app/shared/users/user.service'
10import { peertubeSessionStorage } from '@app/shared/misc/peertube-web-storage'
11import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { SessionStorageService } from '@app/shared/misc/storage.service'
12 12
13@Component({ 13@Component({
14 selector: 'my-recommended-videos', 14 selector: 'my-recommended-videos',
@@ -16,8 +16,6 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
16 styleUrls: [ './recommended-videos.component.scss' ] 16 styleUrls: [ './recommended-videos.component.scss' ]
17}) 17})
18export class RecommendedVideosComponent implements OnChanges { 18export class RecommendedVideosComponent implements OnChanges {
19 static SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO = 'auto_play_next_video'
20
21 @Input() inputRecommendation: RecommendationInfo 19 @Input() inputRecommendation: RecommendationInfo
22 @Input() user: User 20 @Input() user: User
23 @Input() playlist: VideoPlaylist 21 @Input() playlist: VideoPlaylist
@@ -34,15 +32,21 @@ export class RecommendedVideosComponent implements OnChanges {
34 private authService: AuthService, 32 private authService: AuthService,
35 private notifier: Notifier, 33 private notifier: Notifier,
36 private i18n: I18n, 34 private i18n: I18n,
37 private store: RecommendedVideosStore 35 private store: RecommendedVideosStore,
36 private sessionStorageService: SessionStorageService
38 ) { 37 ) {
39 this.videos$ = this.store.recommendations$ 38 this.videos$ = this.store.recommendations$
40 this.hasVideos$ = this.store.hasRecommendations$ 39 this.hasVideos$ = this.store.hasRecommendations$
41 this.videos$.subscribe(videos => this.gotRecommendations.emit(videos)) 40 this.videos$.subscribe(videos => this.gotRecommendations.emit(videos))
42 41
43 this.autoPlayNextVideo = this.authService.isLoggedIn() 42 if (this.authService.isLoggedIn()) {
44 ? this.authService.getUser().autoPlayNextVideo 43 this.autoPlayNextVideo = this.authService.getUser().autoPlayNextVideo
45 : peertubeSessionStorage.getItem(RecommendedVideosComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' || false 44 } else {
45 this.autoPlayNextVideo = this.sessionStorageService.getItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' || false
46 this.sessionStorageService.watch([User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO]).subscribe(
47 () => this.autoPlayNextVideo = this.sessionStorageService.getItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
48 )
49 }
46 50
47 this.autoPlayNextVideoTooltip = this.i18n('When active, the next video is automatically played after the current one.') 51 this.autoPlayNextVideoTooltip = this.i18n('When active, the next video is automatically played after the current one.')
48 } 52 }
@@ -58,7 +62,7 @@ export class RecommendedVideosComponent implements OnChanges {
58 } 62 }
59 63
60 switchAutoPlayNextVideo () { 64 switchAutoPlayNextVideo () {
61 peertubeSessionStorage.setItem(RecommendedVideosComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, this.autoPlayNextVideo.toString()) 65 this.sessionStorageService.setItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, this.autoPlayNextVideo.toString())
62 66
63 if (this.authService.isLoggedIn()) { 67 if (this.authService.isLoggedIn()) {
64 const details = { 68 const details = {
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index 59f65f95c..757b0e498 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -11,6 +11,8 @@ import { ScreenService } from '@app/shared/misc/screen.service'
11import { UserRight } from '../../../../../shared/models/users' 11import { UserRight } from '../../../../../shared/models/users'
12import { Notifier, ServerService } from '@app/core' 12import { Notifier, ServerService } from '@app/core'
13import { HooksService } from '@app/core/plugins/hooks.service' 13import { HooksService } from '@app/core/plugins/hooks.service'
14import { UserService } from '@app/shared'
15import { LocalStorageService } from '@app/shared/misc/storage.service'
14 16
15@Component({ 17@Component({
16 selector: 'my-videos-local', 18 selector: 'my-videos-local',
@@ -31,7 +33,9 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
31 protected route: ActivatedRoute, 33 protected route: ActivatedRoute,
32 protected notifier: Notifier, 34 protected notifier: Notifier,
33 protected authService: AuthService, 35 protected authService: AuthService,
36 protected userService: UserService,
34 protected screenService: ScreenService, 37 protected screenService: ScreenService,
38 protected storageService: LocalStorageService,
35 private videoService: VideoService, 39 private videoService: VideoService,
36 private hooks: HooksService 40 private hooks: HooksService
37 ) { 41 ) {
diff --git a/client/src/app/videos/video-list/video-most-liked.component.ts b/client/src/app/videos/video-list/video-most-liked.component.ts
index 6ff7a1e0e..b69fad05f 100644
--- a/client/src/app/videos/video-list/video-most-liked.component.ts
+++ b/client/src/app/videos/video-list/video-most-liked.component.ts
@@ -9,6 +9,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
9import { ScreenService } from '@app/shared/misc/screen.service' 9import { ScreenService } from '@app/shared/misc/screen.service'
10import { Notifier, ServerService } from '@app/core' 10import { Notifier, ServerService } from '@app/core'
11import { HooksService } from '@app/core/plugins/hooks.service' 11import { HooksService } from '@app/core/plugins/hooks.service'
12import { UserService } from '@app/shared'
13import { LocalStorageService } from '@app/shared/misc/storage.service'
12 14
13@Component({ 15@Component({
14 selector: 'my-videos-most-liked', 16 selector: 'my-videos-most-liked',
@@ -28,7 +30,9 @@ export class VideoMostLikedComponent extends AbstractVideoList implements OnInit
28 protected route: ActivatedRoute, 30 protected route: ActivatedRoute,
29 protected notifier: Notifier, 31 protected notifier: Notifier,
30 protected authService: AuthService, 32 protected authService: AuthService,
33 protected userService: UserService,
31 protected screenService: ScreenService, 34 protected screenService: ScreenService,
35 protected storageService: LocalStorageService,
32 private videoService: VideoService, 36 private videoService: VideoService,
33 private hooks: HooksService 37 private hooks: HooksService
34 ) { 38 ) {
diff --git a/client/src/app/videos/video-list/video-overview.component.html b/client/src/app/videos/video-list/video-overview.component.html
index 5fe1f5c80..84999cfb2 100644
--- a/client/src/app/videos/video-list/video-overview.component.html
+++ b/client/src/app/videos/video-list/video-overview.component.html
@@ -2,35 +2,44 @@
2 2
3 <div class="no-results" i18n *ngIf="notResults">No results.</div> 3 <div class="no-results" i18n *ngIf="notResults">No results.</div>
4 4
5 <div class="section" *ngFor="let object of overview.categories"> 5 <div
6 <div class="section-title"> 6 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"
7 <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> 7 >
8 </div> 8 <ng-container *ngFor="let overview of overviews">
9 9
10 <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false"> 10 <div class="section" *ngFor="let object of overview.categories">
11 </my-video-miniature> 11 <div class="section-title">
12 </div> 12 <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a>
13 </div>
13 14
14 <div class="section" *ngFor="let object of overview.tags"> 15 <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
15 <div class="section-title"> 16 </my-video-miniature>
16 <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> 17 </div>
17 </div>
18 18
19 <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false"> 19 <div class="section" *ngFor="let object of overview.tags">
20 </my-video-miniature> 20 <div class="section-title">
21 </div> 21 <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a>
22 </div>
23
24 <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
25 </my-video-miniature>
26 </div>
27
28 <div class="section channel" *ngFor="let object of overview.channels">
29 <div class="section-title">
30 <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]">
31 <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" />
32
33 <div>{{ object.channel.displayName }}</div>
34 </a>
35 </div>
22 36
23 <div class="section channel" *ngFor="let object of overview.channels"> 37 <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
24 <div class="section-title"> 38 </my-video-miniature>
25 <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> 39 </div>
26 <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" />
27 40
28 <div>{{ object.channel.displayName }}</div> 41 </ng-container>
29 </a>
30 </div>
31 42
32 <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
33 </my-video-miniature>
34 </div> 43 </div>
35 44
36</div> 45</div>
diff --git a/client/src/app/videos/video-list/video-overview.component.ts b/client/src/app/videos/video-list/video-overview.component.ts
index 4fee92d54..101073949 100644
--- a/client/src/app/videos/video-list/video-overview.component.ts
+++ b/client/src/app/videos/video-list/video-overview.component.ts
@@ -5,6 +5,7 @@ import { VideosOverview } from '@app/shared/overview/videos-overview.model'
5import { OverviewService } from '@app/shared/overview' 5import { OverviewService } from '@app/shared/overview'
6import { Video } from '@app/shared/video/video.model' 6import { Video } from '@app/shared/video/video.model'
7import { ScreenService } from '@app/shared/misc/screen.service' 7import { ScreenService } from '@app/shared/misc/screen.service'
8import { Subject } from 'rxjs'
8 9
9@Component({ 10@Component({
10 selector: 'my-video-overview', 11 selector: 'my-video-overview',
@@ -12,13 +13,17 @@ import { ScreenService } from '@app/shared/misc/screen.service'
12 styleUrls: [ './video-overview.component.scss' ] 13 styleUrls: [ './video-overview.component.scss' ]
13}) 14})
14export class VideoOverviewComponent implements OnInit { 15export class VideoOverviewComponent implements OnInit {
15 overview: VideosOverview = { 16 onDataSubject = new Subject<any>()
16 categories: [], 17
17 channels: [], 18 overviews: VideosOverview[] = []
18 tags: []
19 }
20 notResults = false 19 notResults = false
21 20
21 private loaded = false
22 private currentPage = 1
23 private maxPage = 20
24 private lastWasEmpty = false
25 private isLoading = false
26
22 constructor ( 27 constructor (
23 private i18n: I18n, 28 private i18n: I18n,
24 private notifier: Notifier, 29 private notifier: Notifier,
@@ -32,20 +37,7 @@ export class VideoOverviewComponent implements OnInit {
32 } 37 }
33 38
34 ngOnInit () { 39 ngOnInit () {
35 this.overviewService.getVideosOverview() 40 this.loadMoreResults()
36 .subscribe(
37 overview => {
38 this.overview = overview
39
40 if (
41 this.overview.categories.length === 0 &&
42 this.overview.channels.length === 0 &&
43 this.overview.tags.length === 0
44 ) this.notResults = true
45 },
46
47 err => this.notifier.error(err.message)
48 )
49 } 41 }
50 42
51 buildVideoChannelBy (object: { videos: Video[] }) { 43 buildVideoChannelBy (object: { videos: Video[] }) {
@@ -61,4 +53,41 @@ export class VideoOverviewComponent implements OnInit {
61 53
62 return videos.slice(0, numberOfVideos * 2) 54 return videos.slice(0, numberOfVideos * 2)
63 } 55 }
56
57 onNearOfBottom () {
58 if (this.currentPage >= this.maxPage) return
59 if (this.lastWasEmpty) return
60 if (this.isLoading) return
61
62 this.currentPage++
63 this.loadMoreResults()
64 }
65
66 private loadMoreResults () {
67 this.isLoading = true
68
69 this.overviewService.getVideosOverview(this.currentPage)
70 .subscribe(
71 overview => {
72 this.isLoading = false
73
74 if (overview.tags.length === 0 && overview.channels.length === 0 && overview.categories.length === 0) {
75 this.lastWasEmpty = true
76 if (this.loaded === false) this.notResults = true
77
78 return
79 }
80
81 this.loaded = true
82 this.onDataSubject.next(overview)
83
84 this.overviews.push(overview)
85 },
86
87 err => {
88 this.notifier.error(err.message)
89 this.isLoading = false
90 }
91 )
92 }
64} 93}
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
index 7568f4536..c1ddd4fd4 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -9,6 +9,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
9import { ScreenService } from '@app/shared/misc/screen.service' 9import { ScreenService } from '@app/shared/misc/screen.service'
10import { Notifier, ServerService } from '@app/core' 10import { Notifier, ServerService } from '@app/core'
11import { HooksService } from '@app/core/plugins/hooks.service' 11import { HooksService } from '@app/core/plugins/hooks.service'
12import { UserService } from '@app/shared'
13import { LocalStorageService } from '@app/shared/misc/storage.service'
12 14
13@Component({ 15@Component({
14 selector: 'my-videos-recently-added', 16 selector: 'my-videos-recently-added',
@@ -29,7 +31,9 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
29 protected router: Router, 31 protected router: Router,
30 protected notifier: Notifier, 32 protected notifier: Notifier,
31 protected authService: AuthService, 33 protected authService: AuthService,
34 protected userService: UserService,
32 protected screenService: ScreenService, 35 protected screenService: ScreenService,
36 protected storageService: LocalStorageService,
33 private videoService: VideoService, 37 private videoService: VideoService,
34 private hooks: HooksService 38 private hooks: HooksService
35 ) { 39 ) {
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index e29830b5b..fbe052277 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -9,6 +9,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
9import { ScreenService } from '@app/shared/misc/screen.service' 9import { ScreenService } from '@app/shared/misc/screen.service'
10import { Notifier, ServerService } from '@app/core' 10import { Notifier, ServerService } from '@app/core'
11import { HooksService } from '@app/core/plugins/hooks.service' 11import { HooksService } from '@app/core/plugins/hooks.service'
12import { UserService } from '@app/shared'
13import { LocalStorageService } from '@app/shared/misc/storage.service'
12 14
13@Component({ 15@Component({
14 selector: 'my-videos-trending', 16 selector: 'my-videos-trending',
@@ -28,7 +30,9 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
28 protected route: ActivatedRoute, 30 protected route: ActivatedRoute,
29 protected notifier: Notifier, 31 protected notifier: Notifier,
30 protected authService: AuthService, 32 protected authService: AuthService,
33 protected userService: UserService,
31 protected screenService: ScreenService, 34 protected screenService: ScreenService,
35 protected storageService: LocalStorageService,
32 private videoService: VideoService, 36 private videoService: VideoService,
33 private hooks: HooksService 37 private hooks: HooksService
34 ) { 38 ) {
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
index cf0b15054..036fd8dcb 100644
--- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts
+++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
@@ -10,6 +10,8 @@ import { ScreenService } from '@app/shared/misc/screen.service'
10import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' 10import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
11import { Notifier, ServerService } from '@app/core' 11import { Notifier, ServerService } from '@app/core'
12import { HooksService } from '@app/core/plugins/hooks.service' 12import { HooksService } from '@app/core/plugins/hooks.service'
13import { UserService } from '@app/shared'
14import { LocalStorageService } from '@app/shared/misc/storage.service'
13 15
14@Component({ 16@Component({
15 selector: 'my-videos-user-subscriptions', 17 selector: 'my-videos-user-subscriptions',
@@ -29,7 +31,9 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
29 protected route: ActivatedRoute, 31 protected route: ActivatedRoute,
30 protected notifier: Notifier, 32 protected notifier: Notifier,
31 protected authService: AuthService, 33 protected authService: AuthService,
34 protected userService: UserService,
32 protected screenService: ScreenService, 35 protected screenService: ScreenService,
36 protected storageService: LocalStorageService,
33 private videoService: VideoService, 37 private videoService: VideoService,
34 private hooks: HooksService 38 private hooks: HooksService
35 ) { 39 ) {