diff options
Diffstat (limited to 'client/src')
153 files changed, 2352 insertions, 1522 deletions
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.html b/client/src/app/+about/about-instance/contact-admin-modal.component.html index 2b3fb32f3..b2cbd0873 100644 --- a/client/src/app/+about/about-instance/contact-admin-modal.component.html +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4> | 3 | <h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index 036264602..e8339b78b 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -26,8 +26,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
26 | private notifier: Notifier, | 26 | private notifier: Notifier, |
27 | private restExtractor: RestExtractor, | 27 | private restExtractor: RestExtractor, |
28 | private redirectService: RedirectService, | 28 | private redirectService: RedirectService, |
29 | private authService: AuthService, | 29 | private authService: AuthService |
30 | private i18n: I18n | ||
31 | ) {} | 30 | ) {} |
32 | 31 | ||
33 | ngOnInit () { | 32 | ngOnInit () { |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index c06ae1d60..f7f347105 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -10,7 +10,7 @@ import { FollowingListComponent } from './follows/following-list/following-list. | |||
10 | import { JobsComponent } from './jobs/job.component' | 10 | import { JobsComponent } from './jobs/job.component' |
11 | import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' | 11 | import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' |
12 | import { JobService } from './jobs/shared/job.service' | 12 | import { JobService } from './jobs/shared/job.service' |
13 | import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' | 13 | import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users' |
14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' | 14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' |
15 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' | 15 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' |
16 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' | 16 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' |
@@ -36,6 +36,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f | |||
36 | UsersComponent, | 36 | UsersComponent, |
37 | UserCreateComponent, | 37 | UserCreateComponent, |
38 | UserUpdateComponent, | 38 | UserUpdateComponent, |
39 | UserPasswordComponent, | ||
39 | UserListComponent, | 40 | UserListComponent, |
40 | 41 | ||
41 | ModerationComponent, | 42 | ModerationComponent, |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html index 952235c55..303a788d2 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Moderation comment</h4> | 3 | <h4 i18n class="modal-title">Moderation comment</h4> |
4 | <span class="close" aria-hidden="true" (click)="hideModerationCommentModal()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body"> | 8 | <div class="modal-body"> |
@@ -19,7 +20,7 @@ | |||
19 | </div> | 20 | </div> |
20 | 21 | ||
21 | <div class="form-group inputs"> | 22 | <div class="form-group inputs"> |
22 | <span i18n class="action-button action-button-cancel" (click)="hideModerationCommentModal()">Cancel</span> | 23 | <span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span> |
23 | 24 | ||
24 | <input | 25 | <input |
25 | type="submit" i18n-value value="Update this comment" class="action-button-submit" | 26 | type="submit" i18n-value value="Update this comment" class="action-button-submit" |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts index bebcb4207..f915978ee 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts | |||
@@ -45,7 +45,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
45 | }) | 45 | }) |
46 | } | 46 | } |
47 | 47 | ||
48 | hideModerationCommentModal () { | 48 | hide () { |
49 | this.abuseToComment = undefined | 49 | this.abuseToComment = undefined |
50 | this.openedModal.close() | 50 | this.openedModal.close() |
51 | this.form.reset() | 51 | this.form.reset() |
@@ -60,7 +60,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
60 | this.notifier.success(this.i18n('Comment updated.')) | 60 | this.notifier.success(this.i18n('Comment updated.')) |
61 | 61 | ||
62 | this.commentUpdated.emit(moderationComment) | 62 | this.commentUpdated.emit(moderationComment) |
63 | this.hideModerationCommentModal() | 63 | this.hide() |
64 | }, | 64 | }, |
65 | 65 | ||
66 | err => this.notifier.error(err.message) | 66 | err => this.notifier.error(err.message) |
diff --git a/client/src/app/+admin/users/user-edit/index.ts b/client/src/app/+admin/users/user-edit/index.ts index fd80a02e0..ec734ef92 100644 --- a/client/src/app/+admin/users/user-edit/index.ts +++ b/client/src/app/+admin/users/user-edit/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './user-create.component' | 1 | export * from './user-create.component' |
2 | export * from './user-update.component' | 2 | export * from './user-update.component' |
3 | export * from './user-password.component' | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 56cf7d17d..c6566da24 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -81,3 +81,17 @@ | |||
81 | 81 | ||
82 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | 82 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> |
83 | </form> | 83 | </form> |
84 | |||
85 | <div *ngIf="!isCreation()" class="danger-zone"> | ||
86 | <div class="account-title" i18n>Danger Zone</div> | ||
87 | |||
88 | <div class="form-group reset-password-email"> | ||
89 | <label i18n>Send a link to reset the password by email to the user</label> | ||
90 | <button (click)="resetPassword()" i18n>Ask for new password</button> | ||
91 | </div> | ||
92 | |||
93 | <div class="form-group"> | ||
94 | <label i18n>Manually set the user password</label> | ||
95 | <my-user-password [userId]="userId"></my-user-password> | ||
96 | </div> | ||
97 | </div> | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index 6675f65cc..c1cc4ca45 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss | |||
@@ -14,7 +14,7 @@ input:not([type=submit]) { | |||
14 | @include peertube-select-container(340px); | 14 | @include peertube-select-container(340px); |
15 | } | 15 | } |
16 | 16 | ||
17 | input[type=submit] { | 17 | input[type=submit], button { |
18 | @include peertube-button; | 18 | @include peertube-button; |
19 | @include orange-button; | 19 | @include orange-button; |
20 | 20 | ||
@@ -25,3 +25,23 @@ input[type=submit] { | |||
25 | margin-top: 5px; | 25 | margin-top: 5px; |
26 | font-size: 11px; | 26 | font-size: 11px; |
27 | } | 27 | } |
28 | |||
29 | .account-title { | ||
30 | @include in-content-small-title; | ||
31 | |||
32 | margin-top: 55px; | ||
33 | margin-bottom: 30px; | ||
34 | } | ||
35 | |||
36 | .danger-zone { | ||
37 | .reset-password-email { | ||
38 | margin-bottom: 30px; | ||
39 | padding-bottom: 30px; | ||
40 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); | ||
41 | |||
42 | button { | ||
43 | display: block; | ||
44 | margin-top: 0; | ||
45 | } | ||
46 | } | ||
47 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts index 0b3511e8e..649b35b0c 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts | |||
@@ -8,6 +8,7 @@ export abstract class UserEdit extends FormReactive { | |||
8 | videoQuotaDailyOptions: { value: string, label: string }[] = [] | 8 | videoQuotaDailyOptions: { value: string, label: string }[] = [] |
9 | roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) | 9 | roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) |
10 | username: string | 10 | username: string |
11 | userId: number | ||
11 | 12 | ||
12 | protected abstract serverService: ServerService | 13 | protected abstract serverService: ServerService |
13 | protected abstract configService: ConfigService | 14 | protected abstract configService: ConfigService |
@@ -22,7 +23,9 @@ export abstract class UserEdit extends FormReactive { | |||
22 | } | 23 | } |
23 | 24 | ||
24 | computeQuotaWithTranscoding () { | 25 | computeQuotaWithTranscoding () { |
25 | const resolutions = this.serverService.getConfig().transcoding.enabledResolutions | 26 | const transcodingConfig = this.serverService.getConfig().transcoding |
27 | |||
28 | const resolutions = transcodingConfig.enabledResolutions | ||
26 | const higherResolution = VideoResolution.H_1080P | 29 | const higherResolution = VideoResolution.H_1080P |
27 | let multiplier = 0 | 30 | let multiplier = 0 |
28 | 31 | ||
@@ -30,9 +33,15 @@ export abstract class UserEdit extends FormReactive { | |||
30 | multiplier += resolution / higherResolution | 33 | multiplier += resolution / higherResolution |
31 | } | 34 | } |
32 | 35 | ||
36 | if (transcodingConfig.hls.enabled) multiplier *= 2 | ||
37 | |||
33 | return multiplier * parseInt(this.form.value['videoQuota'], 10) | 38 | return multiplier * parseInt(this.form.value['videoQuota'], 10) |
34 | } | 39 | } |
35 | 40 | ||
41 | resetPassword () { | ||
42 | return | ||
43 | } | ||
44 | |||
36 | protected buildQuotaOptions () { | 45 | protected buildQuotaOptions () { |
37 | // These are used by a HTML select, so convert key into strings | 46 | // These are used by a HTML select, so convert key into strings |
38 | this.videoQuotaOptions = this.configService | 47 | this.videoQuotaOptions = this.configService |
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.html b/client/src/app/+admin/users/user-edit/user-password.component.html new file mode 100644 index 000000000..a1e1f6216 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.html | |||
@@ -0,0 +1,21 @@ | |||
1 | <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> | ||
2 | <div class="form-group"> | ||
3 | |||
4 | <div class="input-group"> | ||
5 | <input id="password" [attr.type]="showPassword ? 'text' : 'password'" | ||
6 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
7 | > | ||
8 | <div class="input-group-append"> | ||
9 | <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button"> | ||
10 | <ng-container *ngIf="!showPassword" i18n>Show</ng-container> | ||
11 | <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container> | ||
12 | </button> | ||
13 | </div> | ||
14 | </div> | ||
15 | <div *ngIf="formErrors.password" class="form-error"> | ||
16 | {{ formErrors.password }} | ||
17 | </div> | ||
18 | </div> | ||
19 | |||
20 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | ||
21 | </form> | ||
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss new file mode 100644 index 000000000..217d585af --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.scss | |||
@@ -0,0 +1,22 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | input:not([type=submit]):not([type=checkbox]) { | ||
5 | @include peertube-input-text(340px); | ||
6 | |||
7 | display: block; | ||
8 | border-top-right-radius: 0; | ||
9 | border-bottom-right-radius: 0; | ||
10 | border-right: none; | ||
11 | } | ||
12 | |||
13 | input[type=submit] { | ||
14 | @include peertube-button; | ||
15 | @include orange-button; | ||
16 | |||
17 | margin-top: 10px; | ||
18 | } | ||
19 | |||
20 | .input-group-append { | ||
21 | height: 30px; | ||
22 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts new file mode 100644 index 000000000..5b3040440 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { UserService } from '@app/shared/users/user.service' | ||
4 | import { Notifier } from '../../../core' | ||
5 | import { User, UserUpdate } from '../../../../../../shared' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | ||
9 | import { FormReactive } from '../../../shared' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-user-password', | ||
13 | templateUrl: './user-password.component.html', | ||
14 | styleUrls: [ './user-password.component.scss' ] | ||
15 | }) | ||
16 | export class UserPasswordComponent extends FormReactive implements OnInit { | ||
17 | error: string | ||
18 | username: string | ||
19 | showPassword = false | ||
20 | |||
21 | @Input() userId: number | ||
22 | |||
23 | constructor ( | ||
24 | protected formValidatorService: FormValidatorService, | ||
25 | private userValidatorsService: UserValidatorsService, | ||
26 | private route: ActivatedRoute, | ||
27 | private router: Router, | ||
28 | private notifier: Notifier, | ||
29 | private userService: UserService, | ||
30 | private i18n: I18n | ||
31 | ) { | ||
32 | super() | ||
33 | } | ||
34 | |||
35 | ngOnInit () { | ||
36 | this.buildForm({ | ||
37 | password: this.userValidatorsService.USER_PASSWORD | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | formValidated () { | ||
42 | this.error = undefined | ||
43 | |||
44 | const userUpdate: UserUpdate = this.form.value | ||
45 | |||
46 | this.userService.updateUser(this.userId, userUpdate).subscribe( | ||
47 | () => { | ||
48 | this.notifier.success( | ||
49 | this.i18n('Password changed for user {{username}}.', { username: this.username }) | ||
50 | ) | ||
51 | }, | ||
52 | |||
53 | err => this.error = err.message | ||
54 | ) | ||
55 | } | ||
56 | |||
57 | togglePasswordVisibility () { | ||
58 | this.showPassword = !this.showPassword | ||
59 | } | ||
60 | |||
61 | getFormButtonTitle () { | ||
62 | return this.i18n('Update user password') | ||
63 | } | ||
64 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index 61e641823..94ef87b08 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts | |||
@@ -19,6 +19,7 @@ import { UserService } from '@app/shared' | |||
19 | export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | 19 | export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { |
20 | error: string | 20 | error: string |
21 | userId: number | 21 | userId: number |
22 | userEmail: string | ||
22 | username: string | 23 | username: string |
23 | 24 | ||
24 | private paramsSub: Subscription | 25 | private paramsSub: Subscription |
@@ -89,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
89 | return this.i18n('Update user') | 90 | return this.i18n('Update user') |
90 | } | 91 | } |
91 | 92 | ||
93 | resetPassword () { | ||
94 | this.userService.askResetPassword(this.userEmail).subscribe( | ||
95 | () => { | ||
96 | this.notifier.success( | ||
97 | this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username }) | ||
98 | ) | ||
99 | }, | ||
100 | |||
101 | err => this.error = err.message | ||
102 | ) | ||
103 | } | ||
104 | |||
92 | private onUserFetched (userJson: User) { | 105 | private onUserFetched (userJson: User) { |
93 | this.userId = userJson.id | 106 | this.userId = userJson.id |
94 | this.username = userJson.username | 107 | this.username = userJson.username |
108 | this.userEmail = userJson.email | ||
95 | 109 | ||
96 | this.form.patchValue({ | 110 | this.form.patchValue({ |
97 | email: userJson.email, | 111 | email: userJson.email, |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index 8c03a924b..69a4616a3 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -2,7 +2,7 @@ | |||
2 | <div i18n class="form-sub-title">Users list</div> | 2 | <div i18n class="form-sub-title">Users list</div> |
3 | 3 | ||
4 | <a class="add-button" routerLink="/admin/users/create"> | 4 | <a class="add-button" routerLink="/admin/users/create"> |
5 | <span class="icon icon-add"></span> | 5 | <my-global-icon iconName="add"></my-global-icon> |
6 | <ng-container i18n>Create user</ng-container> | 6 | <ng-container i18n>Create user</ng-container> |
7 | </a> | 7 | </a> |
8 | </div> | 8 | </div> |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss index f235769f0..5274be01c 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/users/user-list/user-list.component.scss | |||
@@ -2,7 +2,7 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .add-button { | 4 | .add-button { |
5 | @include create-button('../../../../assets/images/global/add.svg'); | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | tr.banned { | 8 | tr.banned { |
@@ -23,4 +23,4 @@ tr.banned { | |||
23 | input { | 23 | input { |
24 | @include peertube-input-text(250px); | 24 | @include peertube-input-text(250px); |
25 | } | 25 | } |
26 | } \ No newline at end of file | 26 | } |
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.scss b/client/src/app/+my-account/my-account-history/my-account-history.component.scss index 82150cbe3..e7c6863f1 100644 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.scss +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.scss | |||
@@ -65,10 +65,10 @@ | |||
65 | text-overflow: ellipsis; | 65 | text-overflow: ellipsis; |
66 | white-space: nowrap; | 66 | white-space: nowrap; |
67 | font-size: 14px; | 67 | font-size: 14px; |
68 | color: #585858; | 68 | color: $grey-foreground-color; |
69 | 69 | ||
70 | &:hover { | 70 | &:hover { |
71 | color: #303030; | 71 | color: $grey-foreground-hover-color; |
72 | } | 72 | } |
73 | } | 73 | } |
74 | } | 74 | } |
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html index b98a1087e..d518b22ec 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html | |||
@@ -1,7 +1,13 @@ | |||
1 | <div class="header"> | 1 | <div class="header"> |
2 | <a routerLink="/my-account/settings" fragment="notifications" i18n>Notification preferences</a> | 2 | <a routerLink="/my-account/settings" fragment="notifications" i18n> |
3 | <my-global-icon iconName="cog"></my-global-icon> | ||
4 | Notification preferences | ||
5 | </a> | ||
3 | 6 | ||
4 | <button (click)="markAllAsRead()" i18n>Mark all as read</button> | 7 | <button (click)="markAllAsRead()" i18n> |
8 | <my-global-icon iconName="circle-tick"></my-global-icon> | ||
9 | Mark all as read | ||
10 | </button> | ||
5 | </div> | 11 | </div> |
6 | 12 | ||
7 | <my-user-notifications #userNotification></my-user-notifications> | 13 | <my-user-notifications #userNotification></my-user-notifications> |
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss index 86ac094c5..43d1f82ab 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss | |||
@@ -5,16 +5,18 @@ | |||
5 | display: flex; | 5 | display: flex; |
6 | justify-content: space-between; | 6 | justify-content: space-between; |
7 | font-size: 15px; | 7 | font-size: 15px; |
8 | margin-bottom: 10px; | 8 | margin-bottom: 20px; |
9 | 9 | ||
10 | a { | 10 | a { |
11 | @include peertube-button-link; | 11 | @include peertube-button-link; |
12 | @include grey-button; | 12 | @include grey-button; |
13 | @include button-with-icon(18px, 3px, -1px); | ||
13 | } | 14 | } |
14 | 15 | ||
15 | button { | 16 | button { |
16 | @include peertube-button; | 17 | @include peertube-button; |
17 | @include grey-button; | 18 | @include grey-button; |
19 | @include button-with-icon(20px, 3px, -1px); | ||
18 | } | 20 | } |
19 | } | 21 | } |
20 | 22 | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html index fd7d7d23b..674a4e8a2 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html +++ b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | 1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Accept ownership</h4> | 3 | <h4 i18n class="modal-title">Accept ownership</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body" [formGroup]="form"> | 8 | <div class="modal-body" [formGroup]="form"> |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html index 379fd8bb1..5709e9f54 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html | |||
@@ -40,10 +40,10 @@ | |||
40 | <td class="action-cell"> | 40 | <td class="action-cell"> |
41 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> | 41 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> |
42 | <my-button i18n label="Accept" | 42 | <my-button i18n label="Accept" |
43 | icon="icon-tick" | 43 | icon="tick" |
44 | (click)="openAcceptModal(videoChangeOwnership)"></my-button> | 44 | (click)="openAcceptModal(videoChangeOwnership)"></my-button> |
45 | <my-button i18n label="Refuse" | 45 | <my-button i18n label="Refuse" |
46 | icon="icon-cross" | 46 | icon="cross" |
47 | (click)="refuse(videoChangeOwnership)">Refuse</my-button> | 47 | (click)="refuse(videoChangeOwnership)">Refuse</my-button> |
48 | </ng-container> | 48 | </ng-container> |
49 | </td> | 49 | </td> |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html index df74b19b6..51db2e75d 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div class="video-channels-header"> | 1 | <div class="video-channels-header"> |
2 | <a class="create-button" routerLink="create"> | 2 | <a class="create-button" routerLink="create"> |
3 | <span class="icon icon-add"></span> | 3 | <my-global-icon iconName="add"></my-global-icon> |
4 | <ng-container i18n>Create another video channel</ng-container> | 4 | <ng-container i18n>Create another video channel</ng-container> |
5 | </a> | 5 | </a> |
6 | </div> | 6 | </div> |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss index 472cbb723..77fce138b 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss | |||
@@ -2,7 +2,7 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .create-button { | 4 | .create-button { |
5 | @include create-button('../../../assets/images/global/add.svg'); | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | /deep/ .action-button { | 8 | /deep/ .action-button { |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index a6911e4bf..69748ef37 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html | |||
@@ -32,7 +32,7 @@ | |||
32 | </span> | 32 | </span> |
33 | 33 | ||
34 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> | 34 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> |
35 | <span class="icon icon-delete-white"></span> | 35 | <my-global-icon iconName="delete"></my-global-icon> |
36 | <ng-container i18n>Delete</ng-container> | 36 | <ng-container i18n>Delete</ng-container> |
37 | </span> | 37 | </span> |
38 | </div> | 38 | </div> |
@@ -45,7 +45,7 @@ | |||
45 | 45 | ||
46 | <my-button i18n-label label="Change ownership" | 46 | <my-button i18n-label label="Change ownership" |
47 | className="action-button-change-ownership" | 47 | className="action-button-change-ownership" |
48 | icon="icon-im-with-her" | 48 | icon="im-with-her" |
49 | (click)="changeOwnership($event, video)" | 49 | (click)="changeOwnership($event, video)" |
50 | ></my-button> | 50 | ></my-button> |
51 | </div> | 51 | </div> |
@@ -53,4 +53,4 @@ | |||
53 | </div> | 53 | </div> |
54 | </div> | 54 | </div> |
55 | 55 | ||
56 | <my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership> \ No newline at end of file | 56 | <my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership> |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss index a735562f8..39d0cf2f7 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss | |||
@@ -23,14 +23,11 @@ | |||
23 | .action-button-delete-selection { | 23 | .action-button-delete-selection { |
24 | @include peertube-button; | 24 | @include peertube-button; |
25 | @include orange-button; | 25 | @include orange-button; |
26 | } | 26 | @include button-with-icon(21px); |
27 | |||
28 | .icon.icon-delete-white { | ||
29 | @include icon(21px); | ||
30 | 27 | ||
31 | position: relative; | 28 | my-global-icon { |
32 | top: -2px; | 29 | @include apply-svg-color(#fff); |
33 | background-image: url('../../../assets/images/global/delete-white.svg'); | 30 | } |
34 | } | 31 | } |
35 | } | 32 | } |
36 | } | 33 | } |
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html index 7c0df850d..22f127904 100644 --- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html +++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | 1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Change ownership</h4> | 3 | <h4 i18n class="modal-title">Change ownership</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body" [formGroup]="form"> | 8 | <div class="modal-body" [formGroup]="form"> |
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 3a60139e1..d398d4f35 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html | |||
@@ -30,14 +30,16 @@ | |||
30 | 30 | ||
31 | <footer class="row"> | 31 | <footer class="row"> |
32 | <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a> - | 32 | <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a> - |
33 | <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2018</a> | 33 | <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2019</a> |
34 | </footer> | 34 | </footer> |
35 | </div> | 35 | </div> |
36 | </div> | 36 | </div> |
37 | </div> | 37 | </div> |
38 | 38 | ||
39 | <ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar> | 39 | <ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar> |
40 | |||
40 | <my-confirm></my-confirm> | 41 | <my-confirm></my-confirm> |
42 | |||
41 | <p-toast position="bottom-right"> | 43 | <p-toast position="bottom-right"> |
42 | <ng-template let-message pTemplate="message"> | 44 | <ng-template let-message pTemplate="message"> |
43 | <div class="notification-block"> | 45 | <div class="notification-block"> |
diff --git a/client/src/app/core/confirm/index.ts b/client/src/app/core/confirm/index.ts index 44aabfc13..aca591e1a 100644 --- a/client/src/app/core/confirm/index.ts +++ b/client/src/app/core/confirm/index.ts | |||
@@ -1,2 +1 @@ | |||
1 | export * from './confirm.component' | ||
2 | export * from './confirm.service' | export * from './confirm.service' | |
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 3bc0e2885..4ef3b1e73 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -8,7 +8,7 @@ import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | |||
8 | import { LoadingBarRouterModule } from '@ngx-loading-bar/router' | 8 | import { LoadingBarRouterModule } from '@ngx-loading-bar/router' |
9 | 9 | ||
10 | import { AuthService } from './auth' | 10 | import { AuthService } from './auth' |
11 | import { ConfirmComponent, ConfirmService } from './confirm' | 11 | import { ConfirmService } from './confirm' |
12 | import { throwIfAlreadyLoaded } from './module-import-guard' | 12 | import { throwIfAlreadyLoaded } from './module-import-guard' |
13 | import { LoginGuard, RedirectService, UserRightGuard } from './routing' | 13 | import { LoginGuard, RedirectService, UserRightGuard } from './routing' |
14 | import { ServerService } from './server' | 14 | import { ServerService } from './server' |
@@ -38,7 +38,6 @@ import { UserNotificationSocket } from '@app/core/notification/user-notification | |||
38 | ], | 38 | ], |
39 | 39 | ||
40 | declarations: [ | 40 | declarations: [ |
41 | ConfirmComponent, | ||
42 | CheatSheetComponent | 41 | CheatSheetComponent |
43 | ], | 42 | ], |
44 | 43 | ||
@@ -48,7 +47,6 @@ import { UserNotificationSocket } from '@app/core/notification/user-notification | |||
48 | 47 | ||
49 | ToastModule, | 48 | ToastModule, |
50 | 49 | ||
51 | ConfirmComponent, | ||
52 | CheatSheetComponent | 50 | CheatSheetComponent |
53 | ], | 51 | ], |
54 | 52 | ||
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 4ae72427b..c868ccdcc 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -51,7 +51,10 @@ export class ServerService { | |||
51 | requiresEmailVerification: false | 51 | requiresEmailVerification: false |
52 | }, | 52 | }, |
53 | transcoding: { | 53 | transcoding: { |
54 | enabledResolutions: [] | 54 | enabledResolutions: [], |
55 | hls: { | ||
56 | enabled: false | ||
57 | } | ||
55 | }, | 58 | }, |
56 | avatar: { | 59 | avatar: { |
57 | file: { | 60 | file: { |
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index c23e0c55d..46a87c79c 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html | |||
@@ -5,6 +5,6 @@ | |||
5 | <span (click)="doSearch()" class="icon icon-search"></span> | 5 | <span (click)="doSearch()" class="icon icon-search"></span> |
6 | 6 | ||
7 | <a class="upload-button" routerLink="/videos/upload"> | 7 | <a class="upload-button" routerLink="/videos/upload"> |
8 | <span class="icon icon-upload"></span> | 8 | <my-global-icon iconName="upload"></my-global-icon> |
9 | <span i18n class="upload-button-label">Upload</span> | 9 | <span i18n class="upload-button-label">Upload</span> |
10 | </a> | 10 | </a> |
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index 2f9820665..cea415d9b 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss | |||
@@ -6,6 +6,7 @@ | |||
6 | padding-left: 10px; | 6 | padding-left: 10px; |
7 | margin-right: 15px; | 7 | margin-right: 15px; |
8 | padding-right: 40px; // For the search icon | 8 | padding-right: 40px; // For the search icon |
9 | font-size: 14px; | ||
9 | 10 | ||
10 | &::placeholder { | 11 | &::placeholder { |
11 | color: var(--inputPlaceholderColor); | 12 | color: var(--inputPlaceholderColor); |
@@ -40,6 +41,7 @@ | |||
40 | .upload-button { | 41 | .upload-button { |
41 | @include peertube-button-link; | 42 | @include peertube-button-link; |
42 | @include orange-button; | 43 | @include orange-button; |
44 | @include button-with-icon(22px, 3px, -1px); | ||
43 | 45 | ||
44 | margin-right: 25px; | 46 | margin-right: 25px; |
45 | 47 | ||
@@ -47,15 +49,6 @@ | |||
47 | margin-right: 0; | 49 | margin-right: 0; |
48 | } | 50 | } |
49 | 51 | ||
50 | .icon.icon-upload { | ||
51 | @include icon(22px); | ||
52 | |||
53 | background-image: url('../../assets/images/header/upload-white.svg'); | ||
54 | height: 24px; | ||
55 | vertical-align: middle; | ||
56 | margin-right: 6px; | ||
57 | } | ||
58 | |||
59 | @media screen and (max-width: 600px) { | 52 | @media screen and (max-width: 600px) { |
60 | margin-right: 10px; | 53 | margin-right: 10px; |
61 | padding: 0 10px; | 54 | padding: 0 10px; |
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html index 9b8146624..4efe3fb22 100644 --- a/client/src/app/login/login.component.html +++ b/client/src/app/login/login.component.html | |||
@@ -55,7 +55,8 @@ | |||
55 | <ng-template #forgotPasswordModal> | 55 | <ng-template #forgotPasswordModal> |
56 | <div class="modal-header"> | 56 | <div class="modal-header"> |
57 | <h4 i18n class="modal-title">Forgot your password</h4> | 57 | <h4 i18n class="modal-title">Forgot your password</h4> |
58 | <span class="close" aria-hidden="true" (click)="hideForgotPasswordModal()"></span> | 58 | |
59 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideForgotPasswordModal()"></my-global-icon> | ||
59 | </div> | 60 | </div> |
60 | 61 | ||
61 | <div class="modal-body"> | 62 | <div class="modal-body"> |
diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/avatar-notification.component.scss index 807385022..e785db788 100644 --- a/client/src/app/menu/avatar-notification.component.scss +++ b/client/src/app/menu/avatar-notification.component.scss | |||
@@ -3,7 +3,7 @@ | |||
3 | 3 | ||
4 | /deep/ { | 4 | /deep/ { |
5 | .popover-notifications.popover { | 5 | .popover-notifications.popover { |
6 | max-width: 400px; | 6 | max-width: none; |
7 | 7 | ||
8 | .popover-body { | 8 | .popover-body { |
9 | padding: 0; | 9 | padding: 0; |
@@ -11,9 +11,8 @@ | |||
11 | font-family: $main-fonts; | 11 | font-family: $main-fonts; |
12 | overflow-y: auto; | 12 | overflow-y: auto; |
13 | max-height: 500px; | 13 | max-height: 500px; |
14 | min-width: 200px; | 14 | width: 400px; |
15 | box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30); | 15 | box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30); |
16 | overflow-y: auto; | ||
17 | 16 | ||
18 | .notifications-header { | 17 | .notifications-header { |
19 | display: flex; | 18 | display: flex; |
@@ -42,7 +41,7 @@ | |||
42 | justify-content: center; | 41 | justify-content: center; |
43 | font-weight: $font-semibold; | 42 | font-weight: $font-semibold; |
44 | color: var(--mainForegroundColor); | 43 | color: var(--mainForegroundColor); |
45 | height: 30px; | 44 | padding: 7px 0; |
46 | } | 45 | } |
47 | } | 46 | } |
48 | } | 47 | } |
@@ -73,7 +72,7 @@ | |||
73 | justify-content: center; | 72 | justify-content: center; |
74 | 73 | ||
75 | background-color: var(--mainColor); | 74 | background-color: var(--mainColor); |
76 | color: var(--mainBackgroundColor); | 75 | color: var(#fff); |
77 | font-size: 10px; | 76 | font-size: 10px; |
78 | font-weight: $font-semibold; | 77 | font-weight: $font-semibold; |
79 | 78 | ||
@@ -82,3 +81,11 @@ | |||
82 | height: 15px; | 81 | height: 15px; |
83 | } | 82 | } |
84 | } | 83 | } |
84 | |||
85 | @media screen and (max-width: $mobile-view) { | ||
86 | /deep/ { | ||
87 | .popover-notifications.popover .popover-body { | ||
88 | width: 400px; | ||
89 | } | ||
90 | } | ||
91 | } | ||
diff --git a/client/src/app/menu/language-chooser.component.html b/client/src/app/menu/language-chooser.component.html index c79609898..a62b33dda 100644 --- a/client/src/app/menu/language-chooser.component.html +++ b/client/src/app/menu/language-chooser.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Change the language</h4> | 3 | <h4 i18n class="modal-title">Change the language</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | 7 | ||
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index a4aaadc7f..f30b89413 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss | |||
@@ -16,7 +16,7 @@ menu { | |||
16 | height: 100%; | 16 | height: 100%; |
17 | white-space: nowrap; | 17 | white-space: nowrap; |
18 | text-overflow: ellipsis; | 18 | text-overflow: ellipsis; |
19 | overflow: hidden; | 19 | overflow: auto; |
20 | color: var(--menuForegroundColor); | 20 | color: var(--menuForegroundColor); |
21 | display: flex; | 21 | display: flex; |
22 | flex-direction: column; | 22 | flex-direction: column; |
@@ -243,7 +243,7 @@ menu { | |||
243 | } | 243 | } |
244 | } | 244 | } |
245 | 245 | ||
246 | @media screen and (max-width: 400px) { | 246 | @media screen and (max-width: $mobile-view) { |
247 | .menu-wrapper { | 247 | .menu-wrapper { |
248 | width: 100% !important; | 248 | width: 100% !important; |
249 | } | 249 | } |
diff --git a/client/src/app/search/search.component.scss b/client/src/app/search/search.component.scss index 3e074621b..6de13d276 100644 --- a/client/src/app/search/search.component.scss +++ b/client/src/app/search/search.component.scss | |||
@@ -87,10 +87,10 @@ | |||
87 | text-overflow: ellipsis; | 87 | text-overflow: ellipsis; |
88 | white-space: nowrap; | 88 | white-space: nowrap; |
89 | font-size: 14px; | 89 | font-size: 14px; |
90 | color: #585858; | 90 | color: $grey-foreground-color; |
91 | 91 | ||
92 | &:hover { | 92 | &:hover { |
93 | color: #303030; | 93 | color: $grey-foreground-hover-color; |
94 | } | 94 | } |
95 | } | 95 | } |
96 | } | 96 | } |
diff --git a/client/src/app/shared/actor/actor.model.ts b/client/src/app/shared/actor/actor.model.ts index 811afb449..adecec1fc 100644 --- a/client/src/app/shared/actor/actor.model.ts +++ b/client/src/app/shared/actor/actor.model.ts | |||
@@ -16,7 +16,7 @@ export abstract class Actor implements ActorServer { | |||
16 | 16 | ||
17 | avatarUrl: string | 17 | avatarUrl: string |
18 | 18 | ||
19 | static GET_ACTOR_AVATAR_URL (actor: { avatar: Avatar }) { | 19 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { path: string } }) { |
20 | const absoluteAPIUrl = getAbsoluteAPIUrl() | 20 | const absoluteAPIUrl = getAbsoluteAPIUrl() |
21 | 21 | ||
22 | if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path | 22 | if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path |
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html index 90651f217..114b1d71f 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.html +++ b/client/src/app/shared/buttons/action-dropdown.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }" | 3 | class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }" |
4 | ngbDropdownToggle role="button" | 4 | ngbDropdownToggle role="button" |
5 | > | 5 | > |
6 | <span *ngIf="!label" class="icon icon-action"></span> | 6 | <my-global-icon *ngIf="!label" class="more-icon" iconName="more"></my-global-icon> |
7 | <span *ngIf="label" class="dropdown-toggle">{{ label }}</span> | 7 | <span *ngIf="label" class="dropdown-toggle">{{ label }}</span> |
8 | </div> | 8 | </div> |
9 | 9 | ||
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss index a4fcceeee..985b2ca88 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.scss +++ b/client/src/app/shared/buttons/action-dropdown.component.scss | |||
@@ -24,14 +24,11 @@ | |||
24 | } | 24 | } |
25 | 25 | ||
26 | &:hover, &:active, &:focus { | 26 | &:hover, &:active, &:focus { |
27 | background-color: $grey-color; | 27 | background-color: $grey-background-color; |
28 | } | 28 | } |
29 | 29 | ||
30 | .icon-action { | 30 | .more-icon { |
31 | @include icon(21px); | 31 | width: 21px; |
32 | |||
33 | background-image: url('../../../assets/images/video/more.svg'); | ||
34 | top: -1px; | ||
35 | } | 32 | } |
36 | 33 | ||
37 | &.small { | 34 | &.small { |
diff --git a/client/src/app/shared/buttons/button.component.html b/client/src/app/shared/buttons/button.component.html index 87a8daccf..b6df67102 100644 --- a/client/src/app/shared/buttons/button.component.html +++ b/client/src/app/shared/buttons/button.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <span class="action-button" [ngClass]="className" [title]="getTitle()"> | 1 | <span class="action-button" [ngClass]="className" [title]="getTitle()"> |
2 | <span class="icon" [ngClass]="icon"></span> | 2 | <my-global-icon [iconName]="icon"></my-global-icon> |
3 | <span class="button-label">{{ label }}</span> | 3 | <span class="button-label">{{ label }}</span> |
4 | </span> | 4 | </span> |
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss index 168102f09..04199a2a9 100644 --- a/client/src/app/shared/buttons/button.component.scss +++ b/client/src/app/shared/buttons/button.component.scss | |||
@@ -3,41 +3,18 @@ | |||
3 | 3 | ||
4 | .action-button { | 4 | .action-button { |
5 | @include peertube-button-link; | 5 | @include peertube-button-link; |
6 | @include button-with-icon(21px, 0, -2px); | ||
6 | 7 | ||
7 | font-size: 15px; | ||
8 | font-weight: $font-semibold; | 8 | font-weight: $font-semibold; |
9 | color: #585858; | 9 | color: $grey-foreground-color; |
10 | background-color: #E5E5E5; | 10 | background-color: $grey-background-color; |
11 | 11 | ||
12 | &:hover { | 12 | &:hover { |
13 | background-color: #EFEFEF; | 13 | background-color: $grey-background-hover-color; |
14 | } | 14 | } |
15 | 15 | ||
16 | .icon { | 16 | my-global-icon { |
17 | @include icon(21px); | 17 | @include apply-svg-color($grey-foreground-color); |
18 | |||
19 | position: relative; | ||
20 | top: -2px; | ||
21 | |||
22 | &.icon-edit { | ||
23 | background-image: url('../../../assets/images/global/edit-grey.svg'); | ||
24 | } | ||
25 | |||
26 | &.icon-delete-grey { | ||
27 | background-image: url('../../../assets/images/global/delete-grey.svg'); | ||
28 | } | ||
29 | |||
30 | &.icon-im-with-her { | ||
31 | background-image: url('../../../assets/images/global/im-with-her.svg'); | ||
32 | } | ||
33 | |||
34 | &.icon-tick { | ||
35 | background-image: url('../../../assets/images/global/tick.svg'); | ||
36 | } | ||
37 | |||
38 | &.icon-cross { | ||
39 | background-image: url('../../../assets/images/global/cross.svg'); | ||
40 | } | ||
41 | } | 18 | } |
42 | } | 19 | } |
43 | 20 | ||
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts index 1a1162f09..a91e9c7eb 100644 --- a/client/src/app/shared/buttons/button.component.ts +++ b/client/src/app/shared/buttons/button.component.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { GlobalIconName } from '@app/shared/icons/global-icon.component' | ||
2 | 3 | ||
3 | @Component({ | 4 | @Component({ |
4 | selector: 'my-button', | 5 | selector: 'my-button', |
@@ -9,7 +10,7 @@ import { Component, Input } from '@angular/core' | |||
9 | export class ButtonComponent { | 10 | export class ButtonComponent { |
10 | @Input() label = '' | 11 | @Input() label = '' |
11 | @Input() className: string = undefined | 12 | @Input() className: string = undefined |
12 | @Input() icon: string = undefined | 13 | @Input() icon: GlobalIconName = undefined |
13 | @Input() title: string = undefined | 14 | @Input() title: string = undefined |
14 | 15 | ||
15 | getTitle () { | 16 | getTitle () { |
diff --git a/client/src/app/shared/buttons/delete-button.component.html b/client/src/app/shared/buttons/delete-button.component.html index 6c55d8104..4d12a84c0 100644 --- a/client/src/app/shared/buttons/delete-button.component.html +++ b/client/src/app/shared/buttons/delete-button.component.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <span class="action-button action-button-delete" [title]="getTitle()" role="button"> | 1 | <span class="action-button action-button-delete" [title]="getTitle()" role="button"> |
2 | <span class="icon icon-delete-grey"></span> | 2 | <my-global-icon iconName="delete"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
5 | <span class="button-label" i18n *ngIf="!label">Delete</span> | 5 | <span class="button-label" i18n *ngIf="!label">Delete</span> |
diff --git a/client/src/app/shared/buttons/edit-button.component.html b/client/src/app/shared/buttons/edit-button.component.html index cecb780f3..da3addbae 100644 --- a/client/src/app/shared/buttons/edit-button.component.html +++ b/client/src/app/shared/buttons/edit-button.component.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> | 1 | <a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> |
2 | <span class="icon icon-edit"></span> | 2 | <my-global-icon iconName="edit"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
5 | <span i18n class="button-label" *ngIf="!label">Edit</span> | 5 | <span i18n class="button-label" *ngIf="!label">Edit</span> |
diff --git a/client/src/app/core/confirm/confirm.component.html b/client/src/app/shared/confirm/confirm.component.html index 43f0c6190..65df1cd4d 100644 --- a/client/src/app/core/confirm/confirm.component.html +++ b/client/src/app/shared/confirm/confirm.component.html | |||
@@ -2,7 +2,8 @@ | |||
2 | 2 | ||
3 | <div class="modal-header"> | 3 | <div class="modal-header"> |
4 | <h4 class="modal-title">{{ title }}</h4> | 4 | <h4 class="modal-title">{{ title }}</h4> |
5 | <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> | 5 | |
6 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
6 | </div> | 7 | </div> |
7 | 8 | ||
8 | <div class="modal-body" > | 9 | <div class="modal-body" > |
diff --git a/client/src/app/core/confirm/confirm.component.scss b/client/src/app/shared/confirm/confirm.component.scss index 93dd7926b..93dd7926b 100644 --- a/client/src/app/core/confirm/confirm.component.scss +++ b/client/src/app/shared/confirm/confirm.component.scss | |||
diff --git a/client/src/app/core/confirm/confirm.component.ts b/client/src/app/shared/confirm/confirm.component.ts index 5138b7848..63c163da6 100644 --- a/client/src/app/core/confirm/confirm.component.ts +++ b/client/src/app/shared/confirm/confirm.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core' |
2 | import { ConfirmService } from './confirm.service' | 2 | import { ConfirmService } from '@app/core/confirm/confirm.service' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
diff --git a/client/src/app/shared/icons/global-icon.component.html b/client/src/app/shared/icons/global-icon.component.html new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/client/src/app/shared/icons/global-icon.component.html | |||
diff --git a/client/src/app/shared/icons/global-icon.component.scss b/client/src/app/shared/icons/global-icon.component.scss new file mode 100644 index 000000000..6805fb6f7 --- /dev/null +++ b/client/src/app/shared/icons/global-icon.component.scss | |||
@@ -0,0 +1,4 @@ | |||
1 | /deep/ svg { | ||
2 | width: inherit; | ||
3 | height: inherit; | ||
4 | } | ||
diff --git a/client/src/app/shared/icons/global-icon.component.ts b/client/src/app/shared/icons/global-icon.component.ts new file mode 100644 index 000000000..e8ada0324 --- /dev/null +++ b/client/src/app/shared/icons/global-icon.component.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { Component, ElementRef, Input, OnInit } from '@angular/core' | ||
2 | |||
3 | const icons = { | ||
4 | 'add': require('../../../assets/images/global/add.html'), | ||
5 | 'syndication': require('../../../assets/images/global/syndication.html'), | ||
6 | 'help': require('../../../assets/images/global/help.html'), | ||
7 | 'sparkle': require('../../../assets/images/global/sparkle.html'), | ||
8 | 'alert': require('../../../assets/images/global/alert.html'), | ||
9 | 'cloud-error': require('../../../assets/images/global/cloud-error.html'), | ||
10 | 'user-add': require('../../../assets/images/global/user-add.html'), | ||
11 | 'no': require('../../../assets/images/global/no.html'), | ||
12 | 'cloud-download': require('../../../assets/images/global/cloud-download.html'), | ||
13 | 'undo': require('../../../assets/images/global/undo.html'), | ||
14 | 'circle-tick': require('../../../assets/images/global/circle-tick.html'), | ||
15 | 'cog': require('../../../assets/images/global/cog.html'), | ||
16 | 'download': require('../../../assets/images/global/download.html'), | ||
17 | 'edit': require('../../../assets/images/global/edit.html'), | ||
18 | 'im-with-her': require('../../../assets/images/global/im-with-her.html'), | ||
19 | 'delete': require('../../../assets/images/global/delete.html'), | ||
20 | 'cross': require('../../../assets/images/global/cross.html'), | ||
21 | 'validate': require('../../../assets/images/global/validate.html'), | ||
22 | 'tick': require('../../../assets/images/global/tick.html'), | ||
23 | 'dislike': require('../../../assets/images/video/dislike.html'), | ||
24 | 'heart': require('../../../assets/images/video/heart.html'), | ||
25 | 'like': require('../../../assets/images/video/like.html'), | ||
26 | 'more': require('../../../assets/images/video/more.html'), | ||
27 | 'share': require('../../../assets/images/video/share.html'), | ||
28 | 'upload': require('../../../assets/images/video/upload.html') | ||
29 | } | ||
30 | |||
31 | export type GlobalIconName = keyof typeof icons | ||
32 | |||
33 | @Component({ | ||
34 | selector: 'my-global-icon', | ||
35 | template: '', | ||
36 | styleUrls: [ './global-icon.component.scss' ] | ||
37 | }) | ||
38 | export class GlobalIconComponent implements OnInit { | ||
39 | @Input() iconName: GlobalIconName | ||
40 | |||
41 | constructor (private el: ElementRef) {} | ||
42 | |||
43 | ngOnInit () { | ||
44 | const nativeElement = this.el.nativeElement | ||
45 | |||
46 | nativeElement.innerHTML = icons[this.iconName] | ||
47 | } | ||
48 | } | ||
diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html index 08a2fc367..444425c9f 100644 --- a/client/src/app/shared/misc/help.component.html +++ b/client/src/app/shared/misc/help.component.html | |||
@@ -25,4 +25,6 @@ | |||
25 | [autoClose]="true" | 25 | [autoClose]="true" |
26 | (onHidden)="onPopoverHidden()" | 26 | (onHidden)="onPopoverHidden()" |
27 | (onShown)="onPopoverShown()" | 27 | (onShown)="onPopoverShown()" |
28 | ></span> | 28 | > |
29 | <my-global-icon iconName="help"></my-global-icon> | ||
30 | </span> | ||
diff --git a/client/src/app/shared/misc/help.component.scss b/client/src/app/shared/misc/help.component.scss index 047e53fab..3898f3cda 100644 --- a/client/src/app/shared/misc/help.component.scss +++ b/client/src/app/shared/misc/help.component.scss | |||
@@ -2,13 +2,17 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .help-tooltip-button { | 4 | .help-tooltip-button { |
5 | @include icon(17px); | 5 | cursor: pointer; |
6 | |||
7 | position: relative; | ||
8 | top: -2px; | ||
9 | background-image: url('../../../assets/images/global/help.svg'); | ||
10 | border: none; | 6 | border: none; |
11 | margin: 5px; | 7 | |
8 | my-global-icon { | ||
9 | width: 17px; | ||
10 | position: relative; | ||
11 | top: -2px; | ||
12 | margin: 5px; | ||
13 | |||
14 | @include apply-svg-color(var(--mainForegroundColor)) | ||
15 | } | ||
12 | } | 16 | } |
13 | 17 | ||
14 | /deep/ { | 18 | /deep/ { |
@@ -16,16 +20,21 @@ | |||
16 | max-width: 300px; | 20 | max-width: 300px; |
17 | 21 | ||
18 | .popover-body { | 22 | .popover-body { |
23 | font-family: $main-fonts; | ||
19 | text-align: left; | 24 | text-align: left; |
20 | padding: 10px; | 25 | padding: 10px; |
21 | font-size: 13px; | 26 | font-size: 13px; |
22 | font-family: $main-fonts; | 27 | background-color: var(--mainBackgroundColor); |
23 | background-color: #fff; | 28 | color: var(--mainForegroundColor); |
24 | color: #000; | ||
25 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); | 29 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); |
26 | 30 | ||
31 | p { | ||
32 | margin-bottom: 0; | ||
33 | } | ||
34 | |||
27 | ul { | 35 | ul { |
28 | padding-left: 20px; | 36 | padding-left: 20px; |
37 | margin-bottom: 0; | ||
29 | } | 38 | } |
30 | } | 39 | } |
31 | } | 40 | } |
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.html b/client/src/app/shared/moderation/user-ban-modal.component.html index fa5cb7404..f38ea543d 100644 --- a/client/src/app/shared/moderation/user-ban-modal.component.html +++ b/client/src/app/shared/moderation/user-ban-modal.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Ban</h4> | 3 | <h4 i18n class="modal-title">Ban</h4> |
4 | <span class="close" aria-hidden="true" (click)="hideBanUserModal()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body"> | 8 | <div class="modal-body"> |
@@ -19,7 +20,7 @@ | |||
19 | </div> | 20 | </div> |
20 | 21 | ||
21 | <div class="form-group inputs"> | 22 | <div class="form-group inputs"> |
22 | <span i18n class="action-button action-button-cancel" (click)="hideBanUserModal()">Cancel</span> | 23 | <span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span> |
23 | 24 | ||
24 | <input | 25 | <input |
25 | type="submit" i18n-value value="Ban this user" class="action-button-submit" | 26 | type="submit" i18n-value value="Ban this user" class="action-button-submit" |
@@ -29,4 +30,4 @@ | |||
29 | </form> | 30 | </form> |
30 | </div> | 31 | </div> |
31 | 32 | ||
32 | </ng-template> \ No newline at end of file | 33 | </ng-template> |
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.ts b/client/src/app/shared/moderation/user-ban-modal.component.ts index f755ba0e8..942765301 100644 --- a/client/src/app/shared/moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/moderation/user-ban-modal.component.ts | |||
@@ -42,7 +42,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
42 | this.openedModal = this.modalService.open(this.modal) | 42 | this.openedModal = this.modalService.open(this.modal) |
43 | } | 43 | } |
44 | 44 | ||
45 | hideBanUserModal () { | 45 | hide () { |
46 | this.usersToBan = undefined | 46 | this.usersToBan = undefined |
47 | this.openedModal.close() | 47 | this.openedModal.close() |
48 | } | 48 | } |
@@ -60,7 +60,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
60 | this.notifier.success(message) | 60 | this.notifier.success(message) |
61 | 61 | ||
62 | this.userBanned.emit(this.usersToBan) | 62 | this.userBanned.emit(this.usersToBan) |
63 | this.hideBanUserModal() | 63 | this.hide() |
64 | }, | 64 | }, |
65 | 65 | ||
66 | err => this.notifier.error(err.message) | 66 | err => this.notifier.error(err.message) |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 384f5d722..6f8625c7e 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -67,6 +67,8 @@ import { UserNotificationService } from '@app/shared/users/user-notification.ser | |||
67 | import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' | 67 | import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' |
68 | import { InstanceService } from '@app/shared/instance/instance.service' | 68 | import { InstanceService } from '@app/shared/instance/instance.service' |
69 | import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' | 69 | import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' |
70 | import { ConfirmComponent } from '@app/shared/confirm/confirm.component' | ||
71 | import { GlobalIconComponent } from '@app/shared/icons/global-icon.component' | ||
70 | 72 | ||
71 | @NgModule({ | 73 | @NgModule({ |
72 | imports: [ | 74 | imports: [ |
@@ -110,7 +112,9 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha | |||
110 | UserBanModalComponent, | 112 | UserBanModalComponent, |
111 | UserModerationDropdownComponent, | 113 | UserModerationDropdownComponent, |
112 | TopMenuDropdownComponent, | 114 | TopMenuDropdownComponent, |
113 | UserNotificationsComponent | 115 | UserNotificationsComponent, |
116 | ConfirmComponent, | ||
117 | GlobalIconComponent | ||
114 | ], | 118 | ], |
115 | 119 | ||
116 | exports: [ | 120 | exports: [ |
@@ -151,6 +155,8 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha | |||
151 | UserModerationDropdownComponent, | 155 | UserModerationDropdownComponent, |
152 | TopMenuDropdownComponent, | 156 | TopMenuDropdownComponent, |
153 | UserNotificationsComponent, | 157 | UserNotificationsComponent, |
158 | ConfirmComponent, | ||
159 | GlobalIconComponent, | ||
154 | 160 | ||
155 | NumberFormatterPipe, | 161 | NumberFormatterPipe, |
156 | ObjectLengthPipe, | 162 | ObjectLengthPipe, |
diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts index 5ff816fb8..125d2120c 100644 --- a/client/src/app/shared/users/user-notification.model.ts +++ b/client/src/app/shared/users/user-notification.model.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '../../../../../shared' | 1 | import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo, ActorInfo } from '../../../../../shared' |
2 | import { Actor } from '@app/shared/actor/actor.model' | ||
2 | 3 | ||
3 | export class UserNotification implements UserNotificationServer { | 4 | export class UserNotification implements UserNotificationServer { |
4 | id: number | 5 | id: number |
@@ -6,10 +7,7 @@ export class UserNotification implements UserNotificationServer { | |||
6 | read: boolean | 7 | read: boolean |
7 | 8 | ||
8 | video?: VideoInfo & { | 9 | video?: VideoInfo & { |
9 | channel: { | 10 | channel: ActorInfo & { avatarUrl?: string } |
10 | id: number | ||
11 | displayName: string | ||
12 | } | ||
13 | } | 11 | } |
14 | 12 | ||
15 | videoImport?: { | 13 | videoImport?: { |
@@ -23,10 +21,7 @@ export class UserNotification implements UserNotificationServer { | |||
23 | comment?: { | 21 | comment?: { |
24 | id: number | 22 | id: number |
25 | threadId: number | 23 | threadId: number |
26 | account: { | 24 | account: ActorInfo & { avatarUrl?: string } |
27 | id: number | ||
28 | displayName: string | ||
29 | } | ||
30 | video: VideoInfo | 25 | video: VideoInfo |
31 | } | 26 | } |
32 | 27 | ||
@@ -40,18 +35,11 @@ export class UserNotification implements UserNotificationServer { | |||
40 | video: VideoInfo | 35 | video: VideoInfo |
41 | } | 36 | } |
42 | 37 | ||
43 | account?: { | 38 | account?: ActorInfo & { avatarUrl?: string } |
44 | id: number | ||
45 | displayName: string | ||
46 | name: string | ||
47 | } | ||
48 | 39 | ||
49 | actorFollow?: { | 40 | actorFollow?: { |
50 | id: number | 41 | id: number |
51 | follower: { | 42 | follower: ActorInfo & { avatarUrl?: string } |
52 | name: string | ||
53 | displayName: string | ||
54 | } | ||
55 | following: { | 43 | following: { |
56 | type: 'account' | 'channel' | 44 | type: 'account' | 'channel' |
57 | name: string | 45 | name: string |
@@ -76,12 +64,22 @@ export class UserNotification implements UserNotificationServer { | |||
76 | this.read = hash.read | 64 | this.read = hash.read |
77 | 65 | ||
78 | this.video = hash.video | 66 | this.video = hash.video |
67 | if (this.video) this.setAvatarUrl(this.video.channel) | ||
68 | |||
79 | this.videoImport = hash.videoImport | 69 | this.videoImport = hash.videoImport |
70 | |||
80 | this.comment = hash.comment | 71 | this.comment = hash.comment |
72 | if (this.comment) this.setAvatarUrl(this.comment.account) | ||
73 | |||
81 | this.videoAbuse = hash.videoAbuse | 74 | this.videoAbuse = hash.videoAbuse |
75 | |||
82 | this.videoBlacklist = hash.videoBlacklist | 76 | this.videoBlacklist = hash.videoBlacklist |
77 | |||
83 | this.account = hash.account | 78 | this.account = hash.account |
79 | if (this.account) this.setAvatarUrl(this.account) | ||
80 | |||
84 | this.actorFollow = hash.actorFollow | 81 | this.actorFollow = hash.actorFollow |
82 | if (this.actorFollow) this.setAvatarUrl(this.actorFollow.follower) | ||
85 | 83 | ||
86 | this.createdAt = hash.createdAt | 84 | this.createdAt = hash.createdAt |
87 | this.updatedAt = hash.updatedAt | 85 | this.updatedAt = hash.updatedAt |
@@ -97,6 +95,7 @@ export class UserNotification implements UserNotificationServer { | |||
97 | 95 | ||
98 | case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO: | 96 | case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO: |
99 | case UserNotificationType.COMMENT_MENTION: | 97 | case UserNotificationType.COMMENT_MENTION: |
98 | this.accountUrl = this.buildAccountUrl(this.comment.account) | ||
100 | this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] | 99 | this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] |
101 | break | 100 | break |
102 | 101 | ||
@@ -138,8 +137,8 @@ export class UserNotification implements UserNotificationServer { | |||
138 | return '/videos/watch/' + video.uuid | 137 | return '/videos/watch/' + video.uuid |
139 | } | 138 | } |
140 | 139 | ||
141 | private buildAccountUrl (account: { name: string }) { | 140 | private buildAccountUrl (account: { name: string, host: string }) { |
142 | return '/accounts/' + account.name | 141 | return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host) |
143 | } | 142 | } |
144 | 143 | ||
145 | private buildVideoImportUrl () { | 144 | private buildVideoImportUrl () { |
@@ -150,4 +149,7 @@ export class UserNotification implements UserNotificationServer { | |||
150 | return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName | 149 | return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName |
151 | } | 150 | } |
152 | 151 | ||
152 | private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { path: string } }) { | ||
153 | actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor) | ||
154 | } | ||
153 | } | 155 | } |
diff --git a/client/src/app/shared/users/user-notification.service.ts b/client/src/app/shared/users/user-notification.service.ts index 67ed8f74e..f8a30955d 100644 --- a/client/src/app/shared/users/user-notification.service.ts +++ b/client/src/app/shared/users/user-notification.service.ts | |||
@@ -15,8 +15,6 @@ export class UserNotificationService { | |||
15 | static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications' | 15 | static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications' |
16 | static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings' | 16 | static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings' |
17 | 17 | ||
18 | private socket: SocketIOClient.Socket | ||
19 | |||
20 | constructor ( | 18 | constructor ( |
21 | private auth: AuthService, | 19 | private auth: AuthService, |
22 | private authHttp: HttpClient, | 20 | private authHttp: HttpClient, |
diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html index 86379d941..0d69e0feb 100644 --- a/client/src/app/shared/users/user-notifications.component.html +++ b/client/src/app/shared/users/user-notifications.component.html | |||
@@ -1,61 +1,101 @@ | |||
1 | <div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div> | 1 | <div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div> |
2 | 2 | ||
3 | <div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"> | 3 | <div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"> |
4 | <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }"> | 4 | <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> |
5 | 5 | ||
6 | <div [ngSwitch]="notification.type"> | 6 | <ng-container [ngSwitch]="notification.type"> |
7 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> | 7 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> |
8 | {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a> | 8 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" /> |
9 | |||
10 | <div class="message"> | ||
11 | {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a> | ||
12 | </div> | ||
9 | </ng-container> | 13 | </ng-container> |
10 | 14 | ||
11 | <ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> | 15 | <ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> |
12 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted | 16 | <my-global-icon iconName="undo"></my-global-icon> |
17 | |||
18 | <div class="message"> | ||
19 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted | ||
20 | </div> | ||
13 | </ng-container> | 21 | </ng-container> |
14 | 22 | ||
15 | <ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> | 23 | <ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> |
16 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted | 24 | <my-global-icon iconName="no"></my-global-icon> |
25 | |||
26 | <div class="message"> | ||
27 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted | ||
28 | </div> | ||
17 | </ng-container> | 29 | </ng-container> |
18 | 30 | ||
19 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS"> | 31 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS"> |
20 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a> | 32 | <my-global-icon iconName="alert"></my-global-icon> |
33 | |||
34 | <div class="message"> | ||
35 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a> | ||
36 | </div> | ||
21 | </ng-container> | 37 | </ng-container> |
22 | 38 | ||
23 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> | 39 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> |
24 | {{ notification.comment.account.displayName }} commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a> | 40 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> |
41 | |||
42 | <div class="message"> | ||
43 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a> | ||
44 | </div> | ||
25 | </ng-container> | 45 | </ng-container> |
26 | 46 | ||
27 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> | 47 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> |
28 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published | 48 | <my-global-icon iconName="sparkle"></my-global-icon> |
49 | |||
50 | <div class="message"> | ||
51 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published | ||
52 | </div> | ||
29 | </ng-container> | 53 | </ng-container> |
30 | 54 | ||
31 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> | 55 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> |
32 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded | 56 | <my-global-icon iconName="cloud-download"></my-global-icon> |
57 | |||
58 | <div class="message"> | ||
59 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded | ||
60 | </div> | ||
33 | </ng-container> | 61 | </ng-container> |
34 | 62 | ||
35 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> | 63 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> |
36 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed | 64 | <my-global-icon iconName="cloud-error"></my-global-icon> |
65 | |||
66 | <div class="message"> | ||
67 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed | ||
68 | </div> | ||
37 | </ng-container> | 69 | </ng-container> |
38 | 70 | ||
39 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> | 71 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> |
40 | User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance | 72 | <my-global-icon iconName="user-add"></my-global-icon> |
73 | |||
74 | <div class="message"> | ||
75 | User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance | ||
76 | </div> | ||
41 | </ng-container> | 77 | </ng-container> |
42 | 78 | ||
43 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> | 79 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> |
44 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following | 80 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" /> |
45 | 81 | ||
46 | <ng-container *ngIf="notification.actorFollow.following.type === 'channel'"> | 82 | <div class="message"> |
47 | your channel {{ notification.actorFollow.following.displayName }} | 83 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following |
48 | </ng-container> | 84 | |
49 | <ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container> | 85 | <ng-container *ngIf="notification.actorFollow.following.type === 'channel'">your channel {{ notification.actorFollow.following.displayName }}</ng-container> |
86 | <ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container> | ||
87 | </div> | ||
50 | </ng-container> | 88 | </ng-container> |
51 | 89 | ||
52 | <ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> | 90 | <ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> |
53 | {{ notification.comment.account.displayName }} mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a> | 91 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> |
92 | |||
93 | <div class="message"> | ||
94 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a> | ||
95 | </div> | ||
54 | </ng-container> | 96 | </ng-container> |
55 | </div> | 97 | </ng-container> |
56 | 98 | ||
57 | <div i18n title="Mark as read" class="mark-as-read"> | 99 | <div class="from-date">{{ notification.createdAt | myFromNow }}</div> |
58 | <div class="glyphicon glyphicon-ok" (click)="markAsRead(notification)"></div> | ||
59 | </div> | ||
60 | </div> | 100 | </div> |
61 | </div> | 101 | </div> |
diff --git a/client/src/app/shared/users/user-notifications.component.scss b/client/src/app/shared/users/user-notifications.component.scss index 0ae26ea39..315d504c9 100644 --- a/client/src/app/shared/users/user-notifications.component.scss +++ b/client/src/app/shared/users/user-notifications.component.scss | |||
@@ -1,3 +1,6 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
1 | .no-notification { | 4 | .no-notification { |
2 | display: flex; | 5 | display: flex; |
3 | justify-content: center; | 6 | justify-content: center; |
@@ -7,31 +10,42 @@ | |||
7 | 10 | ||
8 | .notification { | 11 | .notification { |
9 | display: flex; | 12 | display: flex; |
10 | justify-content: space-between; | ||
11 | align-items: center; | 13 | align-items: center; |
12 | font-size: inherit; | 14 | font-size: inherit; |
13 | padding: 15px 10px; | 15 | padding: 15px 5px 15px 10px; |
14 | border-bottom: 1px solid rgba(0, 0, 0, 0.10); | 16 | border-bottom: 1px solid rgba(0, 0, 0, 0.10); |
15 | 17 | ||
16 | .mark-as-read { | 18 | &.unread { |
17 | min-width: 35px; | 19 | background-color: rgba(0, 0, 0, 0.05); |
20 | } | ||
21 | |||
22 | my-global-icon { | ||
23 | width: 24px; | ||
24 | margin-right: 11px; | ||
25 | margin-left: 3px; | ||
18 | 26 | ||
19 | .glyphicon { | 27 | @include apply-svg-color(#333); |
20 | display: none; | ||
21 | cursor: pointer; | ||
22 | color: rgba(20, 20, 20, 0.5) | ||
23 | } | ||
24 | } | 28 | } |
25 | 29 | ||
26 | &.unread { | 30 | .avatar { |
27 | background-color: rgba(0, 0, 0, 0.05); | 31 | @include avatar(30px); |
32 | |||
33 | margin-right: 10px; | ||
34 | } | ||
28 | 35 | ||
29 | &:hover .mark-as-read .glyphicon { | 36 | .message { |
30 | display: block; | 37 | flex-grow: 1; |
31 | 38 | ||
32 | &:hover { | 39 | a { |
33 | color: rgba(20, 20, 20, 0.8); | 40 | font-weight: $font-semibold; |
34 | } | ||
35 | } | 41 | } |
36 | } | 42 | } |
43 | |||
44 | .from-date { | ||
45 | font-size: 0.85em; | ||
46 | color: $grey-foreground-color; | ||
47 | padding-left: 5px; | ||
48 | min-width: 70px; | ||
49 | text-align: right; | ||
50 | } | ||
37 | } | 51 | } |
diff --git a/client/src/app/shared/users/user-notifications.component.ts b/client/src/app/shared/users/user-notifications.component.ts index e3913ba56..b5f9fd399 100644 --- a/client/src/app/shared/users/user-notifications.component.ts +++ b/client/src/app/shared/users/user-notifications.component.ts | |||
@@ -20,11 +20,7 @@ export class UserNotificationsComponent implements OnInit { | |||
20 | // So we can access it in the template | 20 | // So we can access it in the template |
21 | UserNotificationType = UserNotificationType | 21 | UserNotificationType = UserNotificationType |
22 | 22 | ||
23 | componentPagination: ComponentPagination = { | 23 | componentPagination: ComponentPagination |
24 | currentPage: 1, | ||
25 | itemsPerPage: this.itemsPerPage, | ||
26 | totalItems: null | ||
27 | } | ||
28 | 24 | ||
29 | constructor ( | 25 | constructor ( |
30 | private userNotificationService: UserNotificationService, | 26 | private userNotificationService: UserNotificationService, |
@@ -32,6 +28,12 @@ export class UserNotificationsComponent implements OnInit { | |||
32 | ) { } | 28 | ) { } |
33 | 29 | ||
34 | ngOnInit () { | 30 | ngOnInit () { |
31 | this.componentPagination = { | ||
32 | currentPage: 1, | ||
33 | itemsPerPage: this.itemsPerPage, // Reset items per page, because of the @Input() variable | ||
34 | totalItems: null | ||
35 | } | ||
36 | |||
35 | this.loadMoreNotifications() | 37 | this.loadMoreNotifications() |
36 | } | 38 | } |
37 | 39 | ||
@@ -58,6 +60,8 @@ export class UserNotificationsComponent implements OnInit { | |||
58 | } | 60 | } |
59 | 61 | ||
60 | markAsRead (notification: UserNotification) { | 62 | markAsRead (notification: UserNotification) { |
63 | if (notification.read) return | ||
64 | |||
61 | this.userNotificationService.markAsRead(notification) | 65 | this.userNotificationService.markAsRead(notification) |
62 | .subscribe( | 66 | .subscribe( |
63 | () => { | 67 | () => { |
diff --git a/client/src/app/shared/video/feed.component.html b/client/src/app/shared/video/feed.component.html index 16116ba88..f7624ec01 100644 --- a/client/src/app/shared/video/feed.component.html +++ b/client/src/app/shared/video/feed.component.html | |||
@@ -1,10 +1,11 @@ | |||
1 | <div class="video-feed"> | 1 | <div class="video-feed"> |
2 | <span | 2 | <my-global-icon |
3 | *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom" | 3 | *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom" |
4 | class="icon icon-syndication" role="button" | 4 | class="icon-syndication" role="button" iconName="syndication" |
5 | ></span> | 5 | > |
6 | </my-global-icon> | ||
6 | 7 | ||
7 | <ng-template #feedsList> | 8 | <ng-template #feedsList> |
8 | <a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a> | 9 | <a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a> |
9 | </ng-template> | 10 | </ng-template> |
10 | </div> \ No newline at end of file | 11 | </div> |
diff --git a/client/src/app/shared/video/feed.component.scss b/client/src/app/shared/video/feed.component.scss index 385764be0..ed1dc17d3 100644 --- a/client/src/app/shared/video/feed.component.scss +++ b/client/src/app/shared/video/feed.component.scss | |||
@@ -1,3 +1,4 @@ | |||
1 | @import '_variables'; | ||
1 | @import '_mixins'; | 2 | @import '_mixins'; |
2 | 3 | ||
3 | .video-feed { | 4 | .video-feed { |
@@ -6,14 +7,12 @@ | |||
6 | display: block; | 7 | display: block; |
7 | } | 8 | } |
8 | 9 | ||
9 | .icon { | 10 | my-global-icon { |
10 | @include icon(12px); | 11 | cursor: pointer; |
12 | width: 12px; | ||
13 | position: relative; | ||
14 | top: -2px; | ||
11 | 15 | ||
12 | &.icon-syndication { | 16 | @include apply-svg-color(var(--mainForegroundColor)) |
13 | position: relative; | ||
14 | top: -2px; | ||
15 | background-color: var(--mainForegroundColor); | ||
16 | mask-image: url('../../../assets/images/global/syndication.svg'); | ||
17 | } | ||
18 | } | 17 | } |
19 | } \ No newline at end of file | 18 | } |
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index fa4ca7f93..f44b4138b 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts | |||
@@ -3,6 +3,8 @@ import { AuthUser } from '../../core' | |||
3 | import { Video } from '../../shared/video/video.model' | 3 | import { Video } from '../../shared/video/video.model' |
4 | import { Account } from '@app/shared/account/account.model' | 4 | import { Account } from '@app/shared/account/account.model' |
5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
6 | import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model' | ||
7 | import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type' | ||
6 | 8 | ||
7 | export class VideoDetails extends Video implements VideoDetailsServerModel { | 9 | export class VideoDetails extends Video implements VideoDetailsServerModel { |
8 | descriptionPath: string | 10 | descriptionPath: string |
@@ -19,6 +21,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
19 | likesPercent: number | 21 | likesPercent: number |
20 | dislikesPercent: number | 22 | dislikesPercent: number |
21 | 23 | ||
24 | trackerUrls: string[] | ||
25 | |||
26 | streamingPlaylists: VideoStreamingPlaylist[] | ||
27 | |||
22 | constructor (hash: VideoDetailsServerModel, translations = {}) { | 28 | constructor (hash: VideoDetailsServerModel, translations = {}) { |
23 | super(hash, translations) | 29 | super(hash, translations) |
24 | 30 | ||
@@ -30,6 +36,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
30 | this.support = hash.support | 36 | this.support = hash.support |
31 | this.commentsEnabled = hash.commentsEnabled | 37 | this.commentsEnabled = hash.commentsEnabled |
32 | 38 | ||
39 | this.trackerUrls = hash.trackerUrls | ||
40 | this.streamingPlaylists = hash.streamingPlaylists | ||
41 | |||
33 | this.buildLikeAndDislikePercents() | 42 | this.buildLikeAndDislikePercents() |
34 | } | 43 | } |
35 | 44 | ||
@@ -53,4 +62,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
53 | this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 | 62 | this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 |
54 | this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 | 63 | this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 |
55 | } | 64 | } |
65 | |||
66 | getHlsPlaylist () { | ||
67 | return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
68 | } | ||
56 | } | 69 | } |
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss index 895879adc..f44bdf9a9 100644 --- a/client/src/app/shared/video/video-miniature.component.scss +++ b/client/src/app/shared/video/video-miniature.component.scss | |||
@@ -50,10 +50,10 @@ | |||
50 | text-overflow: ellipsis; | 50 | text-overflow: ellipsis; |
51 | white-space: nowrap; | 51 | white-space: nowrap; |
52 | font-size: 13px; | 52 | font-size: 13px; |
53 | color: #585858; | 53 | color: $grey-foreground-color; |
54 | 54 | ||
55 | &:hover { | 55 | &:hover { |
56 | color: #303030; | 56 | color: $grey-foreground-hover-color; |
57 | } | 57 | } |
58 | } | 58 | } |
59 | } | 59 | } |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index b92c96450..6ea83d13b 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -53,7 +53,7 @@ export class Video implements VideoServerModel { | |||
53 | displayName: string | 53 | displayName: string |
54 | url: string | 54 | url: string |
55 | host: string | 55 | host: string |
56 | avatar: Avatar | 56 | avatar?: Avatar |
57 | } | 57 | } |
58 | 58 | ||
59 | channel: { | 59 | channel: { |
@@ -63,7 +63,7 @@ export class Video implements VideoServerModel { | |||
63 | displayName: string | 63 | displayName: string |
64 | url: string | 64 | url: string |
65 | host: string | 65 | host: string |
66 | avatar: Avatar | 66 | avatar?: Avatar |
67 | } | 67 | } |
68 | 68 | ||
69 | userHistory?: { | 69 | userHistory?: { |
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 30aefdbfc..19043eee6 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 | |||
@@ -3,7 +3,7 @@ | |||
3 | 3 | ||
4 | <div class="modal-header"> | 4 | <div class="modal-header"> |
5 | <h4 i18n class="modal-title">Add caption</h4> | 5 | <h4 i18n class="modal-title">Add caption</h4> |
6 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 6 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
7 | </div> | 7 | </div> |
8 | 8 | ||
9 | <div class="modal-body"> | 9 | <div class="modal-body"> |
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 bd52d686a..092c0e862 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 | |||
@@ -143,7 +143,7 @@ | |||
143 | 143 | ||
144 | <div class="captions-header"> | 144 | <div class="captions-header"> |
145 | <a (click)="openAddCaptionModal()" class="create-caption"> | 145 | <a (click)="openAddCaptionModal()" class="create-caption"> |
146 | <span class="icon icon-add"></span> | 146 | <my-global-icon iconName="add"></my-global-icon> |
147 | <ng-container i18n>Add another caption</ng-container> | 147 | <ng-container i18n>Add another caption</ng-container> |
148 | </a> | 148 | </a> |
149 | </div> | 149 | </div> |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index 25db8e8ed..bb775cb0a 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 | |||
@@ -23,10 +23,6 @@ my-peertube-checkbox { | |||
23 | display: block; | 23 | display: block; |
24 | } | 24 | } |
25 | 25 | ||
26 | input, select { | ||
27 | font-size: 15px | ||
28 | } | ||
29 | |||
30 | .label-tags + span { | 26 | .label-tags + span { |
31 | font-size: 15px; | 27 | font-size: 15px; |
32 | } | 28 | } |
@@ -42,7 +38,7 @@ my-peertube-checkbox { | |||
42 | text-align: right; | 38 | text-align: right; |
43 | 39 | ||
44 | .create-caption { | 40 | .create-caption { |
45 | @include create-button('../../../../assets/images/global/add.svg'); | 41 | @include create-button; |
46 | } | 42 | } |
47 | } | 43 | } |
48 | 44 | ||
@@ -100,13 +96,14 @@ my-peertube-checkbox { | |||
100 | display: inline-block; | 96 | display: inline-block; |
101 | margin-right: 25px; | 97 | margin-right: 25px; |
102 | 98 | ||
103 | color: #585858; | 99 | color: $grey-foreground-color; |
104 | font-size: 15px; | 100 | font-size: 15px; |
105 | } | 101 | } |
106 | 102 | ||
107 | .submit-button { | 103 | .submit-button { |
108 | @include peertube-button; | 104 | @include peertube-button; |
109 | @include orange-button; | 105 | @include orange-button; |
106 | @include button-with-icon(20px, 1px); | ||
110 | 107 | ||
111 | display: inline-block; | 108 | display: inline-block; |
112 | 109 | ||
@@ -119,16 +116,6 @@ my-peertube-checkbox { | |||
119 | color: inherit; | 116 | color: inherit; |
120 | font-weight: $font-semibold; | 117 | font-weight: $font-semibold; |
121 | } | 118 | } |
122 | |||
123 | .icon.icon-validate { | ||
124 | @include icon(20px); | ||
125 | |||
126 | cursor: inherit; | ||
127 | position: relative; | ||
128 | top: -1px; | ||
129 | margin-right: 4px; | ||
130 | background-image: url('../../../../assets/images/global/validate.svg'); | ||
131 | } | ||
132 | } | 119 | } |
133 | } | 120 | } |
134 | 121 | ||
@@ -176,10 +163,10 @@ p-calendar { | |||
176 | } | 163 | } |
177 | 164 | ||
178 | tag { | 165 | tag { |
179 | background-color: var(--inputColor) !important; | 166 | background-color: $grey-background-color !important; |
167 | color: #000 !important; | ||
180 | border-radius: 3px !important; | 168 | border-radius: 3px !important; |
181 | font-size: 15px !important; | 169 | font-size: 15px !important; |
182 | color: var(--mainForegroundColor) !important; | ||
183 | height: 30px !important; | 170 | height: 30px !important; |
184 | line-height: 30px !important; | 171 | line-height: 30px !important; |
185 | margin: 0 5px 0 0 !important; | 172 | margin: 0 5px 0 0 !important; |
@@ -202,7 +189,10 @@ p-calendar { | |||
202 | top: -1px; | 189 | top: -1px; |
203 | height: auto !important; | 190 | height: auto !important; |
204 | vertical-align: middle !important; | 191 | vertical-align: middle !important; |
205 | fill: #585858 !important; | 192 | |
193 | path { | ||
194 | fill: $grey-foreground-color !important; | ||
195 | } | ||
206 | } | 196 | } |
207 | 197 | ||
208 | &:hover { | 198 | &:hover { |
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 11a81ad66..28eb143c9 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,6 +1,6 @@ | |||
1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> | 1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> |
2 | <div class="import-video-torrent"> | 2 | <div class="first-step-block"> |
3 | <div class="icon icon-upload"></div> | 3 | <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> |
4 | 4 | ||
5 | <div class="button-file"> | 5 | <div class="button-file"> |
6 | <span i18n>Select the torrent to import</span> | 6 | <span i18n>Select the torrent to import</span> |
@@ -66,7 +66,7 @@ | |||
66 | (click)="updateSecondStep()" | 66 | (click)="updateSecondStep()" |
67 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" | 67 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" |
68 | > | 68 | > |
69 | <span class="icon icon-validate"></span> | 69 | <my-global-icon iconName="validate"></my-global-icon> |
70 | <input type="button" i18n-value value="Update" /> | 70 | <input type="button" i18n-value value="Update" /> |
71 | </div> | 71 | </div> |
72 | </div> | 72 | </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 00626cd7b..6d59ed834 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 | |||
@@ -1,45 +1,7 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | $width-size: 190px; | 4 | .first-step-block { |
5 | |||
6 | .peertube-select-container { | ||
7 | @include peertube-select-container($width-size); | ||
8 | } | ||
9 | |||
10 | .alert.alert-danger { | ||
11 | text-align: center; | ||
12 | |||
13 | & > div { | ||
14 | font-weight: $font-semibold; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .import-video-torrent { | ||
19 | display: flex; | ||
20 | flex-direction: column; | ||
21 | align-items: center; | ||
22 | |||
23 | .icon.icon-upload { | ||
24 | @include icon(90px); | ||
25 | margin-bottom: 25px; | ||
26 | cursor: default; | ||
27 | |||
28 | background-image: url('../../../../assets/images/video/upload.svg'); | ||
29 | } | ||
30 | |||
31 | .button-file { | ||
32 | @include peertube-button-file(auto); | ||
33 | |||
34 | min-width: 190px; | ||
35 | } | ||
36 | |||
37 | .button-file-extension { | ||
38 | display: block; | ||
39 | font-size: 12px; | ||
40 | margin-top: 5px; | ||
41 | } | ||
42 | |||
43 | .torrent-or-magnet { | 5 | .torrent-or-magnet { |
44 | margin: 10px 0; | 6 | margin: 10px 0; |
45 | } | 7 | } |
@@ -47,19 +9,6 @@ $width-size: 190px; | |||
47 | .form-group-magnet-uri { | 9 | .form-group-magnet-uri { |
48 | margin-bottom: 40px; | 10 | margin-bottom: 40px; |
49 | } | 11 | } |
50 | |||
51 | input[type=text] { | ||
52 | @include peertube-input-text($width-size); | ||
53 | display: block; | ||
54 | } | ||
55 | |||
56 | input[type=button] { | ||
57 | @include peertube-button; | ||
58 | @include orange-button; | ||
59 | |||
60 | width: $width-size; | ||
61 | margin-top: 30px; | ||
62 | } | ||
63 | } | 12 | } |
64 | 13 | ||
65 | 14 | ||
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 63db06919..307806bb9 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 | |||
@@ -18,7 +18,8 @@ import { scrollToTop } from '@app/shared/misc/utils' | |||
18 | templateUrl: './video-import-torrent.component.html', | 18 | templateUrl: './video-import-torrent.component.html', |
19 | styleUrls: [ | 19 | styleUrls: [ |
20 | '../shared/video-edit.component.scss', | 20 | '../shared/video-edit.component.scss', |
21 | './video-import-torrent.component.scss' | 21 | './video-import-torrent.component.scss', |
22 | './video-send.scss' | ||
22 | ] | 23 | ] |
23 | }) | 24 | }) |
24 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 25 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { |
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 533446672..3550c3585 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 | |||
@@ -1,6 +1,6 @@ | |||
1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> | 1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> |
2 | <div class="import-video-url"> | 2 | <div class="first-step-block"> |
3 | <div class="icon icon-upload"></div> | 3 | <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> |
4 | 4 | ||
5 | <div class="form-group"> | 5 | <div class="form-group"> |
6 | <label i18n for="targetUrl">URL</label> | 6 | <label i18n for="targetUrl">URL</label> |
@@ -59,7 +59,7 @@ | |||
59 | (click)="updateSecondStep()" | 59 | (click)="updateSecondStep()" |
60 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" | 60 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" |
61 | > | 61 | > |
62 | <span class="icon icon-validate"></span> | 62 | <my-global-icon iconName="validate"></my-global-icon> |
63 | <input type="button" i18n-value value="Update" /> | 63 | <input type="button" i18n-value value="Update" /> |
64 | </div> | 64 | </div> |
65 | </div> | 65 | </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 a1810b7a0..257c6e5db 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 | |||
@@ -18,7 +18,7 @@ import { scrollToTop } from '@app/shared/misc/utils' | |||
18 | templateUrl: './video-import-url.component.html', | 18 | templateUrl: './video-import-url.component.html', |
19 | styleUrls: [ | 19 | styleUrls: [ |
20 | '../shared/video-edit.component.scss', | 20 | '../shared/video-edit.component.scss', |
21 | './video-import-url.component.scss' | 21 | './video-send.scss' |
22 | ] | 22 | ] |
23 | }) | 23 | }) |
24 | export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 24 | export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-send.scss index e907edc70..8769dd302 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss +++ b/client/src/app/videos/+video-edit/video-add-components/video-send.scss | |||
@@ -3,10 +3,6 @@ | |||
3 | 3 | ||
4 | $width-size: 190px; | 4 | $width-size: 190px; |
5 | 5 | ||
6 | .peertube-select-container { | ||
7 | @include peertube-select-container($width-size); | ||
8 | } | ||
9 | |||
10 | .alert.alert-danger { | 6 | .alert.alert-danger { |
11 | text-align: center; | 7 | text-align: center; |
12 | 8 | ||
@@ -15,17 +11,20 @@ $width-size: 190px; | |||
15 | } | 11 | } |
16 | } | 12 | } |
17 | 13 | ||
18 | .import-video-url { | 14 | .first-step-block { |
19 | display: flex; | 15 | display: flex; |
20 | flex-direction: column; | 16 | flex-direction: column; |
21 | align-items: center; | 17 | align-items: center; |
22 | 18 | ||
23 | .icon.icon-upload { | 19 | .upload-icon { |
24 | @include icon(90px); | 20 | width: 90px; |
25 | margin-bottom: 25px; | 21 | margin-bottom: 25px; |
26 | cursor: default; | ||
27 | 22 | ||
28 | background-image: url('../../../../assets/images/video/upload.svg'); | 23 | @include apply-svg-color(#C6C6C6); |
24 | } | ||
25 | |||
26 | .peertube-select-container { | ||
27 | @include peertube-select-container($width-size); | ||
29 | } | 28 | } |
30 | 29 | ||
31 | input[type=text] { | 30 | input[type=text] { |
@@ -40,6 +39,16 @@ $width-size: 190px; | |||
40 | width: $width-size; | 39 | width: $width-size; |
41 | margin-top: 30px; | 40 | margin-top: 30px; |
42 | } | 41 | } |
43 | } | ||
44 | 42 | ||
43 | .button-file { | ||
44 | @include peertube-button-file(auto); | ||
45 | 45 | ||
46 | min-width: 190px; | ||
47 | } | ||
48 | |||
49 | .button-file-extension { | ||
50 | display: block; | ||
51 | font-size: 12px; | ||
52 | margin-top: 5px; | ||
53 | } | ||
54 | } | ||
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 826e54d25..b252cd60a 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,6 +1,6 @@ | |||
1 | <div *ngIf="!isUploadingVideo" class="upload-video-container"> | 1 | <div *ngIf="!isUploadingVideo" class="upload-video-container"> |
2 | <div class="upload-video"> | 2 | <div class="first-step-block"> |
3 | <div class="icon icon-upload"></div> | 3 | <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> |
4 | 4 | ||
5 | <div class="button-file"> | 5 | <div class="button-file"> |
6 | <span i18n>Select the file to upload</span> | 6 | <span i18n>Select the file to upload</span> |
@@ -61,7 +61,7 @@ | |||
61 | (click)="updateSecondStep()" | 61 | (click)="updateSecondStep()" |
62 | [ngClass]="{ disabled: isPublishingButtonDisabled() }" | 62 | [ngClass]="{ disabled: isPublishingButtonDisabled() }" |
63 | > | 63 | > |
64 | <span class="icon icon-validate"></span> | 64 | <my-global-icon iconName="validate"></my-global-icon> |
65 | <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> | 65 | <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> |
66 | </div> | 66 | </div> |
67 | </div> | 67 | </div> |
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 4b2c86ae9..8adf8f169 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 | |||
@@ -1,47 +1,9 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | .peertube-select-container { | 4 | .first-step-block .form-group-channel { |
5 | @include peertube-select-container(190px); | 5 | margin-bottom: 20px; |
6 | } | 6 | margin-top: 35px; |
7 | |||
8 | .alert.alert-danger { | ||
9 | text-align: center; | ||
10 | |||
11 | & > div { | ||
12 | font-weight: $font-semibold; | ||
13 | } | ||
14 | } | ||
15 | |||
16 | .upload-video { | ||
17 | display: flex; | ||
18 | flex-direction: column; | ||
19 | align-items: center; | ||
20 | |||
21 | .form-group-channel { | ||
22 | margin-bottom: 20px; | ||
23 | margin-top: 35px; | ||
24 | } | ||
25 | |||
26 | .icon.icon-upload { | ||
27 | @include icon(90px); | ||
28 | margin-bottom: 25px; | ||
29 | cursor: default; | ||
30 | |||
31 | background-image: url('../../../../assets/images/video/upload.svg'); | ||
32 | } | ||
33 | |||
34 | .button-file { | ||
35 | @include peertube-button-file(auto); | ||
36 | |||
37 | min-width: 190px; | ||
38 | } | ||
39 | |||
40 | .button-file-extension { | ||
41 | display: block; | ||
42 | font-size: 12px; | ||
43 | margin-top: 5px; | ||
44 | } | ||
45 | } | 7 | } |
46 | 8 | ||
47 | .upload-progress-cancel { | 9 | .upload-progress-cancel { |
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 aa40f8781..e4d54b654 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 | |||
@@ -20,7 +20,8 @@ import { scrollToTop } from '@app/shared/misc/utils' | |||
20 | templateUrl: './video-upload.component.html', | 20 | templateUrl: './video-upload.component.html', |
21 | styleUrls: [ | 21 | styleUrls: [ |
22 | '../shared/video-edit.component.scss', | 22 | '../shared/video-edit.component.scss', |
23 | './video-upload.component.scss' | 23 | './video-upload.component.scss', |
24 | './video-send.scss' | ||
24 | ] | 25 | ] |
25 | }) | 26 | }) |
26 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { | 27 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { |
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html index 0457778c0..4992bb369 100644 --- a/client/src/app/videos/+video-edit/video-update.component.html +++ b/client/src/app/videos/+video-edit/video-update.component.html | |||
@@ -13,7 +13,7 @@ | |||
13 | 13 | ||
14 | <div class="submit-container"> | 14 | <div class="submit-container"> |
15 | <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"> | 15 | <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"> |
16 | <span class="icon icon-validate"></span> | 16 | <my-global-icon iconName="validate"></my-global-icon> |
17 | <input type="button" i18n-value value="Update" /> | 17 | <input type="button" i18n-value value="Update" /> |
18 | </div> | 18 | </div> |
19 | </div> | 19 | </div> |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss index 84da5727e..731ecbf8f 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.scss +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.scss | |||
@@ -41,7 +41,7 @@ | |||
41 | } | 41 | } |
42 | 42 | ||
43 | .comment-date { | 43 | .comment-date { |
44 | color: #585858; | 44 | color: $grey-foreground-color; |
45 | margin-left: 10px; | 45 | margin-left: 10px; |
46 | } | 46 | } |
47 | } | 47 | } |
@@ -69,7 +69,7 @@ | |||
69 | 69 | ||
70 | .comment-action-reply, | 70 | .comment-action-reply, |
71 | .comment-action-delete { | 71 | .comment-action-delete { |
72 | color: #585858; | 72 | color: $grey-foreground-color; |
73 | cursor: pointer; | 73 | cursor: pointer; |
74 | margin-right: 10px; | 74 | margin-right: 10px; |
75 | 75 | ||
@@ -108,4 +108,4 @@ | |||
108 | .root-comment { | 108 | .root-comment { |
109 | font-size: 14px; | 109 | font-size: 14px; |
110 | } | 110 | } |
111 | } \ No newline at end of file | 111 | } |
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html index 83600fa81..1a87bdcd4 100644 --- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html +++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Blacklist video</h4> | 3 | <h4 i18n class="modal-title">Blacklist video</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/videos/+video-watch/modal/video-download.component.html index f46f92a17..2bb5d6d37 100644 --- a/client/src/app/videos/+video-watch/modal/video-download.component.html +++ b/client/src/app/videos/+video-watch/modal/video-download.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Download video</h4> | 3 | <h4 i18n class="modal-title">Download video</h4> |
4 | <span class="close" aria-hidden="true" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html index 733c01be0..b9434da26 100644 --- a/client/src/app/videos/+video-watch/modal/video-report.component.html +++ b/client/src/app/videos/+video-watch/modal/video-report.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Report video</h4> | 3 | <h4 i18n class="modal-title">Report video</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
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 301f67f2d..9f3c37fe8 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 | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Share</h4> | 3 | <h4 i18n class="modal-title">Share</h4> |
4 | <span class="close" aria-hidden="true" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
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 00c304709..2a05224a8 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 | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Support</h4> | 3 | <h4 i18n class="modal-title">Support</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> | 7 | <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> |
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 770785d02..709eb91a8 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html | |||
@@ -53,55 +53,57 @@ | |||
53 | <div | 53 | <div |
54 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" | 54 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" |
55 | class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" | 55 | class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" |
56 | i18n-title title="Like this video" | ||
56 | > | 57 | > |
57 | <span class="icon icon-like" i18n-title title="Like this video" ></span> | 58 | <my-global-icon iconName="like"></my-global-icon> |
58 | </div> | 59 | </div> |
59 | 60 | ||
60 | <div | 61 | <div |
61 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" | 62 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" |
62 | class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" | 63 | class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" |
64 | i18n-title title="Dislike this video" | ||
63 | > | 65 | > |
64 | <span class="icon icon-dislike" i18n-title title="Dislike this video"></span> | 66 | <my-global-icon iconName="dislike"></my-global-icon> |
65 | </div> | 67 | </div> |
66 | 68 | ||
67 | <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> | 69 | <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> |
68 | <span class="icon icon-support"></span> | 70 | <my-global-icon iconName="heart"></my-global-icon> |
69 | <span class="icon-text" i18n>Support</span> | 71 | <span class="icon-text" i18n>Support</span> |
70 | </div> | 72 | </div> |
71 | 73 | ||
72 | <div (click)="showShareModal()" class="action-button action-button-share" role="button"> | 74 | <div (click)="showShareModal()" class="action-button action-button-share" role="button"> |
73 | <span class="icon icon-share"></span> | 75 | <my-global-icon iconName="share"></my-global-icon> |
74 | <span class="icon-text" i18n>Share</span> | 76 | <span class="icon-text" i18n>Share</span> |
75 | </div> | 77 | </div> |
76 | 78 | ||
77 | <div class="action-more" ngbDropdown placement="top" role="button"> | 79 | <div class="action-more" ngbDropdown placement="top" role="button"> |
78 | <div class="action-button" ngbDropdownToggle role="button"> | 80 | <div class="action-button" ngbDropdownToggle role="button"> |
79 | <span class="icon icon-more"></span> | 81 | <my-global-icon class="more-icon" iconName="more"></my-global-icon> |
80 | </div> | 82 | </div> |
81 | 83 | ||
82 | <div ngbDropdownMenu> | 84 | <div ngbDropdownMenu> |
83 | <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> | 85 | <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> |
84 | <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container> | 86 | <my-global-icon iconName="download"></my-global-icon> <ng-container i18n>Download</ng-container> |
85 | </a> | 87 | </a> |
86 | 88 | ||
87 | <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> | 89 | <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> |
88 | <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container> | 90 | <my-global-icon iconName="alert"></my-global-icon> <ng-container i18n>Report</ng-container> |
89 | </a> | 91 | </a> |
90 | 92 | ||
91 | <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> | 93 | <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> |
92 | <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container> | 94 | <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Update</ng-container> |
93 | </a> | 95 | </a> |
94 | 96 | ||
95 | <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> | 97 | <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> |
96 | <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container> | 98 | <my-global-icon iconName="no"></my-global-icon> <ng-container i18n>Blacklist</ng-container> |
97 | </a> | 99 | </a> |
98 | 100 | ||
99 | <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> | 101 | <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> |
100 | <span class="icon icon-unblacklist"></span> <ng-container i18n>Unblacklist</ng-container> | 102 | <my-global-icon iconName="undo"></my-global-icon> <ng-container i18n>Unblacklist</ng-container> |
101 | </a> | 103 | </a> |
102 | 104 | ||
103 | <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> | 105 | <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> |
104 | <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container> | 106 | <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete</ng-container> |
105 | </a> | 107 | </a> |
106 | </div> | 108 | </div> |
107 | </div> | 109 | </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 2586a2204..b03ed197d 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss | |||
@@ -183,6 +183,8 @@ $other-videos-width: 260px; | |||
183 | .action-button { | 183 | .action-button { |
184 | @include peertube-button; | 184 | @include peertube-button; |
185 | @include grey-button; | 185 | @include grey-button; |
186 | @include button-with-icon(21px, 0, -1px); | ||
187 | @include apply-svg-color($grey-foreground-color); | ||
186 | 188 | ||
187 | font-size: 15px; | 189 | font-size: 15px; |
188 | font-weight: $font-semibold; | 190 | font-weight: $font-semibold; |
@@ -194,53 +196,25 @@ $other-videos-width: 260px; | |||
194 | display: none; | 196 | display: none; |
195 | } | 197 | } |
196 | 198 | ||
197 | .icon { | ||
198 | @include icon(21px); | ||
199 | |||
200 | position: relative; | ||
201 | top: -2px; | ||
202 | |||
203 | &.icon-like { | ||
204 | background-image: url('../../../assets/images/video/like-grey.svg'); | ||
205 | } | ||
206 | |||
207 | &.icon-dislike { | ||
208 | background-image: url('../../../assets/images/video/dislike-grey.svg'); | ||
209 | } | ||
210 | |||
211 | &.icon-support { | ||
212 | background-image: url('../../../assets/images/video/heart.svg'); | ||
213 | } | ||
214 | |||
215 | &.icon-share { | ||
216 | background-image: url('../../../assets/images/video/share.svg'); | ||
217 | } | ||
218 | |||
219 | &.icon-more { | ||
220 | background-image: url('../../../assets/images/video/more.svg'); | ||
221 | top: -1px; | ||
222 | } | ||
223 | } | ||
224 | |||
225 | .icon-text { | ||
226 | margin-left: 3px; | ||
227 | } | ||
228 | |||
229 | &.action-button-like.activated { | 199 | &.action-button-like.activated { |
230 | background-color: $green; | 200 | background-color: $green; |
231 | 201 | ||
232 | .icon-like { | 202 | my-global-icon { |
233 | background-image: url('../../../assets/images/video/like-white.svg'); | 203 | @include apply-svg-color(#fff); |
234 | } | 204 | } |
235 | } | 205 | } |
236 | 206 | ||
237 | &.action-button-dislike.activated { | 207 | &.action-button-dislike.activated { |
238 | background-color: $red; | 208 | background-color: $red; |
239 | 209 | ||
240 | .icon-dislike { | 210 | my-global-icon { |
241 | background-image: url('../../../assets/images/video/dislike-white.svg'); | 211 | @include apply-svg-color(#fff); |
242 | } | 212 | } |
243 | } | 213 | } |
214 | |||
215 | .icon-text { | ||
216 | margin-left: 3px; | ||
217 | } | ||
244 | } | 218 | } |
245 | 219 | ||
246 | .action-more { | 220 | .action-more { |
@@ -249,36 +223,12 @@ $other-videos-width: 260px; | |||
249 | .dropdown-menu .dropdown-item { | 223 | .dropdown-menu .dropdown-item { |
250 | padding: 6px 24px; | 224 | padding: 6px 24px; |
251 | 225 | ||
252 | .icon { | 226 | my-global-icon { |
253 | @include icon(24px); | 227 | width: 24px; |
254 | 228 | ||
255 | margin-right: 10px; | 229 | margin-right: 10px; |
256 | position: relative; | 230 | position: relative; |
257 | top: -1px; | 231 | top: -2px; |
258 | |||
259 | &.icon-download { | ||
260 | background-image: url('../../../assets/images/video/download-black.svg'); | ||
261 | } | ||
262 | |||
263 | &.icon-edit { | ||
264 | background-image: url('../../../assets/images/global/edit-black.svg'); | ||
265 | } | ||
266 | |||
267 | &.icon-alert { | ||
268 | background-image: url('../../../assets/images/video/alert.svg'); | ||
269 | } | ||
270 | |||
271 | &.icon-blacklist { | ||
272 | background-image: url('../../../assets/images/video/blacklist.svg'); | ||
273 | } | ||
274 | |||
275 | &.icon-unblacklist { | ||
276 | background-image: url('../../../assets/images/global/undo.svg'); | ||
277 | } | ||
278 | |||
279 | &.icon-delete { | ||
280 | background-image: url('../../../assets/images/global/delete-black.svg'); | ||
281 | } | ||
282 | } | 232 | } |
283 | } | 233 | } |
284 | } | 234 | } |
@@ -320,7 +270,7 @@ $other-videos-width: 260px; | |||
320 | .video-info-description-more { | 270 | .video-info-description-more { |
321 | cursor: pointer; | 271 | cursor: pointer; |
322 | font-weight: $font-semibold; | 272 | font-weight: $font-semibold; |
323 | color: #585858; | 273 | color: $grey-foreground-color; |
324 | font-size: 14px; | 274 | font-size: 14px; |
325 | 275 | ||
326 | .glyphicon { | 276 | .glyphicon { |
@@ -339,7 +289,7 @@ $other-videos-width: 260px; | |||
339 | min-width: 91px; | 289 | min-width: 91px; |
340 | padding-right: 5px; | 290 | padding-right: 5px; |
341 | display: inline-block; | 291 | display: inline-block; |
342 | color: #585858; | 292 | color: $grey-foreground-color; |
343 | font-weight: $font-bold; | 293 | font-weight: $font-bold; |
344 | } | 294 | } |
345 | 295 | ||
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 ee504bc58..e801f03ad 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -7,14 +7,8 @@ import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-supp | |||
7 | import { MetaService } from '@ngx-meta/core' | 7 | import { MetaService } from '@ngx-meta/core' |
8 | import { Notifier, ServerService } from '@app/core' | 8 | import { Notifier, ServerService } from '@app/core' |
9 | import { forkJoin, Subscription } from 'rxjs' | 9 | import { forkJoin, Subscription } from 'rxjs' |
10 | // FIXME: something weird with our path definition in tsconfig and typings | ||
11 | // @ts-ignore | ||
12 | import videojs from 'video.js' | ||
13 | import 'videojs-hotkeys' | ||
14 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 10 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
15 | import * as WebTorrent from 'webtorrent' | ||
16 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' | 11 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' |
17 | import '../../../assets/player/peertube-videojs-plugin' | ||
18 | import { AuthService, ConfirmService } from '../../core' | 12 | import { AuthService, ConfirmService } from '../../core' |
19 | import { RestExtractor, VideoBlacklistService } from '../../shared' | 13 | import { RestExtractor, VideoBlacklistService } from '../../shared' |
20 | import { VideoDetails } from '../../shared/video/video-details.model' | 14 | import { VideoDetails } from '../../shared/video/video-details.model' |
@@ -24,12 +18,16 @@ import { VideoReportComponent } from './modal/video-report.component' | |||
24 | import { VideoShareComponent } from './modal/video-share.component' | 18 | import { VideoShareComponent } from './modal/video-share.component' |
25 | import { VideoBlacklistComponent } from './modal/video-blacklist.component' | 19 | import { VideoBlacklistComponent } from './modal/video-blacklist.component' |
26 | import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' | 20 | import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' |
27 | import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player' | ||
28 | import { I18n } from '@ngx-translate/i18n-polyfill' | 21 | import { I18n } from '@ngx-translate/i18n-polyfill' |
29 | import { environment } from '../../../environments/environment' | 22 | import { environment } from '../../../environments/environment' |
30 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | ||
31 | import { VideoCaptionService } from '@app/shared/video-caption' | 23 | import { VideoCaptionService } from '@app/shared/video-caption' |
32 | import { MarkdownService } from '@app/shared/renderer' | 24 | import { MarkdownService } from '@app/shared/renderer' |
25 | import { | ||
26 | P2PMediaLoaderOptions, | ||
27 | PeertubePlayerManager, | ||
28 | PeertubePlayerManagerOptions, | ||
29 | PlayerMode | ||
30 | } from '../../../assets/player/peertube-player-manager' | ||
33 | 31 | ||
34 | @Component({ | 32 | @Component({ |
35 | selector: 'my-video-watch', | 33 | selector: 'my-video-watch', |
@@ -46,7 +44,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
46 | @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent | 44 | @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent |
47 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 45 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
48 | 46 | ||
49 | player: videojs.Player | 47 | player: any |
50 | playerElement: HTMLVideoElement | 48 | playerElement: HTMLVideoElement |
51 | userRating: UserVideoRateType = null | 49 | userRating: UserVideoRateType = null |
52 | video: VideoDetails = null | 50 | video: VideoDetails = null |
@@ -61,7 +59,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
61 | remoteServerDown = false | 59 | remoteServerDown = false |
62 | hotkeys: Hotkey[] | 60 | hotkeys: Hotkey[] |
63 | 61 | ||
64 | private videojsLocaleLoaded = false | ||
65 | private paramsSub: Subscription | 62 | private paramsSub: Subscription |
66 | 63 | ||
67 | constructor ( | 64 | constructor ( |
@@ -92,7 +89,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
92 | 89 | ||
93 | ngOnInit () { | 90 | ngOnInit () { |
94 | if ( | 91 | if ( |
95 | WebTorrent.WEBRTC_SUPPORT === false || | 92 | !!((window as any).RTCPeerConnection || (window as any).mozRTCPeerConnection || (window as any).webkitRTCPeerConnection) === false || |
96 | peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' | 93 | peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' |
97 | ) { | 94 | ) { |
98 | this.hasAlreadyAcceptedPrivacyConcern = true | 95 | this.hasAlreadyAcceptedPrivacyConcern = true |
@@ -118,8 +115,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
118 | .subscribe(([ video, captionsResult ]) => { | 115 | .subscribe(([ video, captionsResult ]) => { |
119 | const startTime = this.route.snapshot.queryParams.start | 116 | const startTime = this.route.snapshot.queryParams.start |
120 | const subtitle = this.route.snapshot.queryParams.subtitle | 117 | const subtitle = this.route.snapshot.queryParams.subtitle |
118 | const playerMode = this.route.snapshot.queryParams.mode | ||
121 | 119 | ||
122 | this.onVideoFetched(video, captionsResult.data, { startTime, subtitle }) | 120 | this.onVideoFetched(video, captionsResult.data, { startTime, subtitle, playerMode }) |
123 | .catch(err => this.handleError(err)) | 121 | .catch(err => this.handleError(err)) |
124 | }) | 122 | }) |
125 | }) | 123 | }) |
@@ -366,7 +364,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
366 | ) | 364 | ) |
367 | } | 365 | } |
368 | 366 | ||
369 | private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], urlOptions: { startTime: number, subtitle: string }) { | 367 | private async onVideoFetched ( |
368 | video: VideoDetails, | ||
369 | videoCaptions: VideoCaption[], | ||
370 | urlOptions: { startTime?: number, subtitle?: string, playerMode?: string } | ||
371 | ) { | ||
370 | this.video = video | 372 | this.video = video |
371 | 373 | ||
372 | // Re init attributes | 374 | // Re init attributes |
@@ -402,41 +404,64 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
402 | src: environment.apiUrl + c.captionPath | 404 | src: environment.apiUrl + c.captionPath |
403 | })) | 405 | })) |
404 | 406 | ||
405 | const videojsOptions = getVideojsOptions({ | 407 | const options: PeertubePlayerManagerOptions = { |
406 | autoplay: this.isAutoplay(), | 408 | common: { |
407 | inactivityTimeout: 2500, | 409 | autoplay: this.isAutoplay(), |
408 | videoFiles: this.video.files, | 410 | |
409 | videoCaptions: playerCaptions, | 411 | playerElement: this.playerElement, |
410 | playerElement: this.playerElement, | 412 | onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element, |
411 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, | 413 | |
412 | videoDuration: this.video.duration, | 414 | videoDuration: this.video.duration, |
413 | enableHotkeys: true, | 415 | enableHotkeys: true, |
414 | peertubeLink: false, | 416 | inactivityTimeout: 2500, |
415 | poster: this.video.previewUrl, | 417 | poster: this.video.previewUrl, |
416 | startTime, | 418 | startTime, |
417 | subtitle: urlOptions.subtitle, | 419 | |
418 | theaterMode: true, | 420 | theaterMode: true, |
419 | language: this.localeId, | 421 | captions: videoCaptions.length !== 0, |
420 | 422 | peertubeLink: false, | |
421 | userWatching: this.user && this.user.videosHistoryEnabled === true ? { | 423 | |
422 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), | 424 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, |
423 | authorizationHeader: this.authService.getRequestHeaderValue() | 425 | embedUrl: this.video.embedUrl, |
424 | } : undefined | 426 | |
425 | }) | 427 | language: this.localeId, |
428 | |||
429 | subtitle: urlOptions.subtitle, | ||
430 | |||
431 | userWatching: this.user && this.user.videosHistoryEnabled === true ? { | ||
432 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), | ||
433 | authorizationHeader: this.authService.getRequestHeaderValue() | ||
434 | } : undefined, | ||
435 | |||
436 | serverUrl: environment.apiUrl, | ||
426 | 437 | ||
427 | if (this.videojsLocaleLoaded === false) { | 438 | videoCaptions: playerCaptions |
428 | await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId) | 439 | }, |
429 | this.videojsLocaleLoaded = true | 440 | |
441 | webtorrent: { | ||
442 | videoFiles: this.video.files | ||
443 | } | ||
430 | } | 444 | } |
431 | 445 | ||
432 | const self = this | 446 | const mode: PlayerMode = urlOptions.playerMode === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' |
433 | this.zone.runOutsideAngular(async () => { | 447 | |
434 | videojs(this.playerElement, videojsOptions, function (this: videojs.Player) { | 448 | if (mode === 'p2p-media-loader') { |
435 | self.player = this | 449 | const hlsPlaylist = this.video.getHlsPlaylist() |
436 | this.on('customError', ({ err }: { err: any }) => self.handleError(err)) | ||
437 | 450 | ||
438 | addContextMenu(self.player, self.video.embedUrl) | 451 | const p2pMediaLoader = { |
439 | }) | 452 | playlistUrl: hlsPlaylist.playlistUrl, |
453 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
454 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
455 | trackerAnnounce: this.video.trackerUrls, | ||
456 | videoFiles: this.video.files | ||
457 | } as P2PMediaLoaderOptions | ||
458 | |||
459 | Object.assign(options, { p2pMediaLoader }) | ||
460 | } | ||
461 | |||
462 | this.zone.runOutsideAngular(async () => { | ||
463 | this.player = await PeertubePlayerManager.initialize(mode, options) | ||
464 | this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) | ||
440 | }) | 465 | }) |
441 | 466 | ||
442 | this.setVideoDescriptionHTML() | 467 | this.setVideoDescriptionHTML() |
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 881ab2174..6fd74e67a 100644 --- a/client/src/app/videos/video-list/video-trending.component.ts +++ b/client/src/app/videos/video-list/video-trending.component.ts | |||
@@ -39,18 +39,21 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, | |||
39 | 39 | ||
40 | this.generateSyndicationList() | 40 | this.generateSyndicationList() |
41 | 41 | ||
42 | const trendingDays = this.serverService.getConfig().trending.videos.intervalDays | 42 | this.serverService.configLoaded.subscribe( |
43 | () => { | ||
44 | const trendingDays = this.serverService.getConfig().trending.videos.intervalDays | ||
43 | 45 | ||
44 | if (trendingDays === 1) { | 46 | if (trendingDays === 1) { |
45 | this.titlePage = this.i18n('Trending for the last 24 hours') | 47 | this.titlePage = this.i18n('Trending for the last 24 hours') |
46 | this.titleTooltip = this.i18n('Trending videos are those totalizing the greatest number of views during the last 24 hours.') | 48 | this.titleTooltip = this.i18n('Trending videos are those totalizing the greatest number of views during the last 24 hours.') |
47 | } else { | 49 | } else { |
48 | this.titlePage = this.i18n('Trending for the last {{days}} days', { days: trendingDays }) | 50 | this.titlePage = this.i18n('Trending for the last {{days}} days', { days: trendingDays }) |
49 | this.titleTooltip = this.i18n( | 51 | this.titleTooltip = this.i18n( |
50 | 'Trending videos are those totalizing the greatest number of views during the last {{days}} days.', | 52 | 'Trending videos are those totalizing the greatest number of views during the last {{days}} days.', |
51 | { days: trendingDays } | 53 | { days: trendingDays } |
52 | ) | 54 | ) |
53 | } | 55 | } |
56 | }) | ||
54 | } | 57 | } |
55 | 58 | ||
56 | ngOnDestroy () { | 59 | ngOnDestroy () { |
diff --git a/client/src/assets/images/global/add.svg b/client/src/assets/images/global/add.html index 42b269c43..bfb0a52bc 100644 --- a/client/src/assets/images/global/add.svg +++ b/client/src/assets/images/global/add.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-92.000000, -115.000000)"> |
5 | <g id="Artboard-4" transform="translate(-92.000000, -115.000000)"> | ||
6 | <g id="2" transform="translate(92.000000, 115.000000)"> | 4 | <g id="2" transform="translate(92.000000, 115.000000)"> |
7 | <circle id="Oval-1" stroke="#ffffff" stroke-width="2" cx="12" cy="12" r="10"></circle> | 5 | <circle id="Oval-1" stroke="#ffffff" stroke-width="2" cx="12" cy="12" r="10"></circle> |
8 | <rect id="Rectangle-1" fill="#ffffff" x="11" y="7" width="2" height="10" rx="1"></rect> | 6 | <rect id="Rectangle-1" fill="#ffffff" x="11" y="7" width="2" height="10" rx="1"></rect> |
diff --git a/client/src/assets/images/video/alert.svg b/client/src/assets/images/global/alert.html index 5b43534ad..7c8c02074 100644 --- a/client/src/assets/images/video/alert.svg +++ b/client/src/assets/images/global/alert.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <title>alert</title> | 3 | <g transform="translate(-48.000000, -467.000000)"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -467.000000)"> | ||
9 | <g id="161" transform="translate(48.000000, 467.000000)"> | 4 | <g id="161" transform="translate(48.000000, 467.000000)"> |
10 | <path d="M12.8715755,3.50973876 L12,1.96027114 L11.1284245,3.50973876 L2.12842446,19.5097388 L1.29015252,21 L3,21 L21,21 L22.7098475,21 L21.8715755,19.5097388 L12.8715755,3.50973876 Z" id="Triangle-2" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path> | 5 | <path d="M12.8715755,3.50973876 L12,1.96027114 L11.1284245,3.50973876 L2.12842446,19.5097388 L1.29015252,21 L3,21 L21,21 L22.7098475,21 L21.8715755,19.5097388 L12.8715755,3.50973876 Z" id="Triangle-2" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path> |
11 | <path d="M12,17.75 C12.6903559,17.75 13.25,17.1903559 13.25,16.5 C13.25,15.8096441 12.6903559,15.25 12,15.25 C11.3096441,15.25 10.75,15.8096441 10.75,16.5 C10.75,17.1903559 11.3096441,17.75 12,17.75 Z" id="Oval-8" fill="#000000"></path> | 6 | <path d="M12,17.75 C12.6903559,17.75 13.25,17.1903559 13.25,16.5 C13.25,15.8096441 12.6903559,15.25 12,15.25 C11.3096441,15.25 10.75,15.8096441 10.75,16.5 C10.75,17.1903559 11.3096441,17.75 12,17.75 Z" id="Oval-8" fill="#000000"></path> |
diff --git a/client/src/assets/images/global/circle-tick.html b/client/src/assets/images/global/circle-tick.html new file mode 100644 index 000000000..2327de6be --- /dev/null +++ b/client/src/assets/images/global/circle-tick.html | |||
@@ -0,0 +1,12 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-400.000000, -1134.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
5 | <g id="yes" transform="translate(352.000000, 88.000000)"> | ||
6 | <circle id="Oval-1" cx="12" cy="12" r="10"/> | ||
7 | <polyline id="Path-288" stroke-linecap="round" stroke-linejoin="round" points="8.5 12.5 10.5 14.5 15.5 9.5"/> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </g> | ||
12 | </svg> | ||
diff --git a/client/src/assets/images/global/cloud-download.html b/client/src/assets/images/global/cloud-download.html new file mode 100644 index 000000000..b2634fd1f --- /dev/null +++ b/client/src/assets/images/global/cloud-download.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
3 | <g transform="translate(-356.000000, -775.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="308" transform="translate(356.000000, 775.000000)"> | ||
5 | <path d="M8,17 L5,17 L5,17 C2.790861,17 1,15.209139 1,13 C1,10.790861 2.790861,9 5,9 C5.35840468,9 5.70579988,9.04713713 6.03632437,9.13555013 C6.01233106,8.92702603 6,8.71495305 6,8.5 C6,5.46243388 8.46243388,3 11.5,3 C14.0673313,3 16.2238156,4.7590449 16.8299648,7.1376465 C17.2052921,7.04765874 17.5970804,7 18,7 C20.7614237,7 23,9.23857625 23,12 C23,14.7614237 20.7614237,17 18,17 L16,17" id="Combined-Shape" stroke-linejoin="round"></path> | ||
6 | <path d="M12,13 L12,21" id="Path-58"></path> | ||
7 | <polyline id="Path-59" stroke-linejoin="round" points="15 20 12 23 9 20"></polyline> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/cloud-error.html b/client/src/assets/images/global/cloud-error.html new file mode 100644 index 000000000..1a3483805 --- /dev/null +++ b/client/src/assets/images/global/cloud-error.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> | ||
3 | <g transform="translate(-400.000000, -775.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="309" transform="translate(400.000000, 775.000000)"> | ||
5 | <path d="M7,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L17,18" id="Combined-Shape"></path> | ||
6 | <path d="M9,21 L15,15" id="Path-238"></path> | ||
7 | <path d="M9,21 L15,15" id="Path-238" transform="translate(12.000000, 18.000000) scale(-1, 1) translate(-12.000000, -18.000000) "></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/cog.html b/client/src/assets/images/global/cog.html new file mode 100644 index 000000000..b74a180e7 --- /dev/null +++ b/client/src/assets/images/global/cog.html | |||
@@ -0,0 +1,9 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round"> | ||
3 | <g transform="translate(-796.000000, -159.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="38" transform="translate(796.000000, 159.000000)"> | ||
5 | <path d="M7.20852293,4.3800958 C8.05442158,3.84706631 8.99528987,3.45099725 10,3.22301642 L10,1.99980749 C10,1.44762906 10.4433532,1 11.0093689,1 L12.9906311,1 C13.5480902,1 14,1.44371665 14,1.99980749 L14,3.22301642 C15.0047101,3.45099725 15.9455784,3.84706631 16.7914771,4.3800958 L17.6569904,3.5145825 C18.0474395,3.12413339 18.6774591,3.12110988 19.0776926,3.52134344 L20.4786566,4.92230738 C20.8728396,5.31649045 20.8786331,5.94979402 20.4854175,6.34300963 L19.6199042,7.20852293 C20.1529337,8.05442158 20.5490027,8.99528987 20.7769836,10 L22.0001925,10 C22.5523709,10 23,10.4433532 23,11.0093689 L23,12.9906311 C23,13.5480902 22.5562834,14 22.0001925,14 L20.7769836,14 C20.5490027,15.0047101 20.1529337,15.9455784 19.6199042,16.7914771 L20.4854175,17.6569904 C20.8758666,18.0474395 20.8788901,18.6774591 20.4786566,19.0776926 L19.0776926,20.4786566 C18.6835095,20.8728396 18.050206,20.8786331 17.6569904,20.4854175 L16.7914771,19.6199042 C15.9455784,20.1529337 15.0047101,20.5490027 14,20.7769836 L14,22.0001925 C14,22.5523709 13.5566468,23 12.9906311,23 L11.0093689,23 C10.4519098,23 10,22.5562834 10,22.0001925 L10,20.7769836 C8.99528987,20.5490027 8.05442158,20.1529337 7.20852293,19.6199042 L6.34300963,20.4854175 C5.95256051,20.8758666 5.32254093,20.8788901 4.92230738,20.4786566 L3.52134344,19.0776926 C3.12716036,18.6835095 3.12136689,18.050206 3.5145825,17.6569904 L4.3800958,16.7914771 C3.84706631,15.9455784 3.45099725,15.0047101 3.22301642,14 L1.99980749,14 C1.44762906,14 1,13.5566468 1,12.9906311 L1,11.0093689 C1,10.4519098 1.44371665,10 1.99980749,10 L3.22301642,10 C3.45099725,8.99528987 3.84706631,8.05442158 4.3800958,7.20852293 L3.5145825,6.34300963 C3.12413339,5.95256051 3.12110988,5.32254093 3.52134344,4.92230738 L4.92230738,3.52134344 C5.31649045,3.12716036 5.94979402,3.12136689 6.34300963,3.5145825 L7.20852293,4.3800958 Z M12,16 C14.209139,16 16,14.209139 16,12 C16,9.790861 14.209139,8 12,8 C9.790861,8 8,9.790861 8,12 C8,14.209139 9.790861,16 12,16 Z" id="Combined-Shape"/> | ||
6 | </g> | ||
7 | </g> | ||
8 | </g> | ||
9 | </svg> | ||
diff --git a/client/src/assets/images/global/cross.svg b/client/src/assets/images/global/cross.html index d47a75996..962578487 100644 --- a/client/src/assets/images/global/cross.svg +++ b/client/src/assets/images/global/cross.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | 3 | <g transform="translate(-312.000000, -115.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-312.000000, -115.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="7" transform="translate(312.000000, 115.000000)"> | 4 | <g id="7" transform="translate(312.000000, 115.000000)"> |
7 | <path d="M19,5 L5,19" id="Path-14"></path> | 5 | <path d="M19,5 L5,19" id="Path-14"></path> |
8 | <path d="M19,5 L5,19" id="Path-14" transform="translate(12.000000, 12.000000) scale(-1, 1) translate(-12.000000, -12.000000) "></path> | 6 | <path d="M19,5 L5,19" id="Path-14" transform="translate(12.000000, 12.000000) scale(-1, 1) translate(-12.000000, -12.000000) "></path> |
diff --git a/client/src/assets/images/global/delete-black.svg b/client/src/assets/images/global/delete-black.svg deleted file mode 100644 index 04ddc23aa..000000000 --- a/client/src/assets/images/global/delete-black.svg +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | ||
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#000" stroke-width="2"></path> | ||
8 | <rect id="Rectangle-424" fill="#000" x="2" y="4" width="20" height="2" rx="1"></rect> | ||
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#000"></path> | ||
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#000" stroke-width="2" stroke-linejoin="round"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/global/delete-grey.svg b/client/src/assets/images/global/delete-grey.svg deleted file mode 100644 index 67e9e2ce7..000000000 --- a/client/src/assets/images/global/delete-grey.svg +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | ||
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#585858" stroke-width="2"></path> | ||
8 | <rect id="Rectangle-424" fill="#585858" x="2" y="4" width="20" height="2" rx="1"></rect> | ||
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#585858"></path> | ||
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#585858" stroke-width="2" stroke-linejoin="round"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/global/delete-white.svg b/client/src/assets/images/global/delete.html index 9c52de557..a0d9a0cac 100644 --- a/client/src/assets/images/global/delete-white.svg +++ b/client/src/assets/images/global/delete.html | |||
@@ -1,13 +1,11 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-224.000000, -159.000000)"> |
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | 4 | <g id="25" transform="translate(224.000000, 159.000000)"> |
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#ffffff" stroke-width="2"></path> | 5 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#000000" stroke-width="2"></path> |
8 | <rect id="Rectangle-424" fill="#ffffff" x="2" y="4" width="20" height="2" rx="1"></rect> | 6 | <rect id="Rectangle-424" fill="#000000" x="2" y="4" width="20" height="2" rx="1"></rect> |
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#ffffff"></path> | 7 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#000000"></path> |
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"></path> | 8 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path> |
11 | </g> | 9 | </g> |
12 | </g> | 10 | </g> |
13 | </g> | 11 | </g> |
diff --git a/client/src/assets/images/video/download-black.svg b/client/src/assets/images/global/download.html index 501836746..259506f31 100644 --- a/client/src/assets/images/video/download-black.svg +++ b/client/src/assets/images/global/download.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>download</title> | 3 | <g transform="translate(-180.000000, -291.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#000000" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | 4 | <g id="84" transform="translate(180.000000, 291.000000)"> |
10 | <path d="M12,3 L12,15" id="Path-58"></path> | 5 | <path d="M12,3 L12,15" id="Path-58"></path> |
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | 6 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> |
diff --git a/client/src/assets/images/global/edit-black.svg b/client/src/assets/images/global/edit-black.svg deleted file mode 100644 index 0176b0f37..000000000 --- a/client/src/assets/images/global/edit-black.svg +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>edit</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -203.000000)" stroke="#000000" stroke-width="2"> | ||
9 | <g id="41" transform="translate(48.000000, 203.000000)"> | ||
10 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> | ||
11 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/global/edit-grey.svg b/client/src/assets/images/global/edit.html index 23ece68f1..f04183c2d 100644 --- a/client/src/assets/images/global/edit-grey.svg +++ b/client/src/assets/images/global/edit.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <title>edit</title> | 3 | <g transform="translate(-48.000000, -203.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -203.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="41" transform="translate(48.000000, 203.000000)"> | 4 | <g id="41" transform="translate(48.000000, 203.000000)"> |
10 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> | 5 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> |
11 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> | 6 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> |
diff --git a/client/src/assets/images/global/help.svg b/client/src/assets/images/global/help.html index 48252febe..80cd40321 100644 --- a/client/src/assets/images/global/help.svg +++ b/client/src/assets/images/global/help.html | |||
@@ -1,12 +1,10 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-400.000000, -247.000000)"> |
5 | <g id="Artboard-4" transform="translate(-400.000000, -247.000000)"> | ||
6 | <g id="69" transform="translate(400.000000, 247.000000)"> | 4 | <g id="69" transform="translate(400.000000, 247.000000)"> |
7 | <circle id="Oval-7" stroke="#333333" stroke-width="2" cx="12" cy="12" r="10"></circle> | 5 | <circle id="Oval-7" stroke="#000000" stroke-width="2" cx="12" cy="12" r="10"></circle> |
8 | <path d="M12.016,14.544 C12.384,14.544 12.64,14.256 12.704,13.904 L12.768,13.168 C14.544,12.864 16,11.952 16,9.936 L16,9.904 C16,7.904 14.48,6.656 12.24,6.656 C10.768,6.656 9.696,7.184 8.848,7.984 C8.624,8.176 8.528,8.432 8.528,8.672 C8.528,9.152 8.928,9.552 9.424,9.552 C9.648,9.552 9.856,9.456 10.016,9.328 C10.656,8.752 11.344,8.448 12.192,8.448 C13.344,8.448 14.032,9.072 14.032,9.968 L14.032,10 C14.032,11.008 13.2,11.584 11.696,11.728 C11.264,11.776 11.008,12.096 11.072,12.528 L11.232,13.904 C11.28,14.272 11.552,14.544 11.92,14.544 L12.016,14.544 Z M10.784,16.816 L10.784,16.976 C10.784,17.6 11.264,18.08 11.92,18.08 C12.576,18.08 13.056,17.6 13.056,16.976 L13.056,16.816 C13.056,16.192 12.576,15.712 11.92,15.712 C11.264,15.712 10.784,16.192 10.784,16.816 Z" id="?" fill="#333333"></path> | 6 | <path d="M12.016,14.544 C12.384,14.544 12.64,14.256 12.704,13.904 L12.768,13.168 C14.544,12.864 16,11.952 16,9.936 L16,9.904 C16,7.904 14.48,6.656 12.24,6.656 C10.768,6.656 9.696,7.184 8.848,7.984 C8.624,8.176 8.528,8.432 8.528,8.672 C8.528,9.152 8.928,9.552 9.424,9.552 C9.648,9.552 9.856,9.456 10.016,9.328 C10.656,8.752 11.344,8.448 12.192,8.448 C13.344,8.448 14.032,9.072 14.032,9.968 L14.032,10 C14.032,11.008 13.2,11.584 11.696,11.728 C11.264,11.776 11.008,12.096 11.072,12.528 L11.232,13.904 C11.28,14.272 11.552,14.544 11.92,14.544 L12.016,14.544 Z M10.784,16.816 L10.784,16.976 C10.784,17.6 11.264,18.08 11.92,18.08 C12.576,18.08 13.056,17.6 13.056,16.976 L13.056,16.816 C13.056,16.192 12.576,15.712 11.92,15.712 C11.264,15.712 10.784,16.192 10.784,16.816 Z" id="?" fill="#000000"></path> |
9 | </g> | 7 | </g> |
10 | </g> | 8 | </g> |
11 | </g> | 9 | </g> |
12 | </svg> \ No newline at end of file | 10 | </svg> |
diff --git a/client/src/assets/images/global/im-with-her.svg b/client/src/assets/images/global/im-with-her.html index 31d4754fd..de2c62e96 100644 --- a/client/src/assets/images/global/im-with-her.svg +++ b/client/src/assets/images/global/im-with-her.html | |||
@@ -1,15 +1,10 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <title>im-with-her</title> | 3 | <g transform="translate(-708.000000, -467.000000)"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -467.000000)"> | ||
9 | <g id="176" transform="translate(708.000000, 467.000000)"> | 4 | <g id="176" transform="translate(708.000000, 467.000000)"> |
10 | <path d="M8,9 L8,3.99339768 C8,3.44494629 7.55641359,3 7.00922203,3 L2.99077797,3 C2.45097518,3 2,3.44475929 2,3.99339768 L2,20.0066023 C2,20.5550537 2.44358641,21 2.99077797,21 L7.00922203,21 C7.54902482,21 8,20.5552407 8,20.0066023 L8,15 L14,15 L14,20.0066023 C14,20.5550537 14.4435864,21 14.990778,21 L19.009222,21 C19.5490248,21 20,20.5564587 20,20.0093228 L20,15.0006104 L23,12 L20,8.99267578 L20,4.00303919 C20,3.45042467 19.5564136,3 19.009222,3 L14.990778,3 C14.4509752,3 14,3.44475929 14,3.99339768 L14,9 L8,9 Z" id="Combined-Shape" fill="#333333" opacity="0.5"></path> | 5 | <path d="M8,9 L8,3.99339768 C8,3.44494629 7.55641359,3 7.00922203,3 L2.99077797,3 C2.45097518,3 2,3.44475929 2,3.99339768 L2,20.0066023 C2,20.5550537 2.44358641,21 2.99077797,21 L7.00922203,21 C7.54902482,21 8,20.5552407 8,20.0066023 L8,15 L14,15 L14,20.0066023 C14,20.5550537 14.4435864,21 14.990778,21 L19.009222,21 C19.5490248,21 20,20.5564587 20,20.0093228 L20,15.0006104 L23,12 L20,8.99267578 L20,4.00303919 C20,3.45042467 19.5564136,3 19.009222,3 L14.990778,3 C14.4509752,3 14,3.44475929 14,3.99339768 L14,9 L8,9 Z" id="Combined-Shape" fill="#000000" opacity="0.5"></path> |
11 | <path d="M2,9 L14,9 L14,3.99077797 C14,3.44358641 14.3203148,3.32031476 14.7062149,3.7062149 L23,12 L14.7062149,20.2937851 C14.3161832,20.6838168 14,20.5490248 14,20.009222 L14,15 L2,15 L2,9 Z" id="Rectangle-121" fill-opacity="0.5" fill="#000000"></path> | 6 | <path d="M2,9 L14,9 L14,3.99077797 C14,3.44358641 14.3203148,3.32031476 14.7062149,3.7062149 L23,12 L14.7062149,20.2937851 C14.3161832,20.6838168 14,20.5490248 14,20.009222 L14,15 L2,15 L2,9 Z" id="Rectangle-121" fill-opacity="0.5" fill="#000000"></path> |
12 | </g> | 7 | </g> |
13 | </g> | 8 | </g> |
14 | </g> | 9 | </g> |
15 | </svg> \ No newline at end of file | 10 | </svg> |
diff --git a/client/src/assets/images/global/no.html b/client/src/assets/images/global/no.html new file mode 100644 index 000000000..bb7b28514 --- /dev/null +++ b/client/src/assets/images/global/no.html | |||
@@ -0,0 +1,10 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-312.000000, -863.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="347" transform="translate(312.000000, 863.000000)"> | ||
5 | <circle id="Oval-196" cx="12" cy="12" r="9"></circle> | ||
6 | <path d="M18,18 L6,6" id="Path-275"></path> | ||
7 | </g> | ||
8 | </g> | ||
9 | </g> | ||
10 | </svg> | ||
diff --git a/client/src/assets/images/global/sparkle.html b/client/src/assets/images/global/sparkle.html new file mode 100644 index 000000000..3b29fefb9 --- /dev/null +++ b/client/src/assets/images/global/sparkle.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> | ||
3 | <g transform="translate(-488.000000, -731.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="291" transform="translate(488.000000, 731.000000)"> | ||
5 | <path d="M10,9 C8.5,7.5 8,3 8,3 C8,3 7.5,7.5 6,9 C4.5,10.5 2,11 2,11 C2,11 4.5,11.5 6,13 C7.5,14.5 8,19 8,19 C8,19 8.5,14.5 10,13 C11.5,11.5 14,11 14,11 C14,11 11.5,10.5 10,9 Z" id="Combined-Shape"></path> | ||
6 | <path d="M19.6666667,4.75 C18.7916667,3.8125 18.5,1 18.5,1 C18.5,1 18.2083333,3.8125 17.3333333,4.75 C16.4583333,5.6875 15,6 15,6 C15,6 16.4583333,6.3125 17.3333333,7.25 C18.2083333,8.1875 18.5,11 18.5,11 C18.5,11 18.7916667,8.1875 19.6666667,7.25 C20.5416667,6.3125 22,6 22,6 C22,6 20.5416667,5.6875 19.6666667,4.75 Z" id="Combined-Shape"></path> | ||
7 | <path d="M17,17 C16.25,16.25 16,14 16,14 C16,14 15.75,16.25 15,17 C14.25,17.75 13,18 13,18 C13,18 14.25,18.25 15,19 C15.75,19.75 16,22 16,22 C16,22 16.25,19.75 17,19 C17.75,18.25 19,18 19,18 C19,18 17.75,17.75 17,17 Z" id="Combined-Shape"></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/syndication.svg b/client/src/assets/images/global/syndication.html index cb74cf81b..e6c88a4db 100644 --- a/client/src/assets/images/global/syndication.svg +++ b/client/src/assets/images/global/syndication.html | |||
@@ -1,10 +1,8 @@ | |||
1 | <?xml version="1.0" encoding="iso-8859-1"?> | ||
2 | <!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | ||
3 | <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | 1 | <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" |
4 | viewBox="0 0 559.372 559.372" style="enable-background:new 0 0 559.372 559.372;" xml:space="preserve"> | 2 | viewBox="0 0 559.372 559.372" style="enable-background:new 0 0 559.372 559.372;" xml:space="preserve"> |
5 | <g> | 3 | <g> |
6 | <g> | 4 | <g> |
7 | <path style="fill:#010002;" d="M53.244,0.002c46.512,0,91.29,6.018,134.334,18.054s83.334,29.07,120.869,51.102 | 5 | <path fill="#000000" d="M53.244,0.002c46.512,0,91.29,6.018,134.334,18.054s83.334,29.07,120.869,51.102 |
8 | c37.537,22.032,71.707,48.45,102.514,79.254c30.803,30.804,57.221,64.974,79.254,102.51 | 6 | c37.537,22.032,71.707,48.45,102.514,79.254c30.803,30.804,57.221,64.974,79.254,102.51 |
9 | c22.029,37.539,39.063,77.828,51.102,120.873c12.037,43.043,18.055,87.818,18.055,134.334c0,14.688-5.201,27.23-15.605,37.637 | 7 | c22.029,37.539,39.063,77.828,51.102,120.873c12.037,43.043,18.055,87.818,18.055,134.334c0,14.688-5.201,27.23-15.605,37.637 |
10 | c-10.404,10.407-22.949,15.604-37.637,15.604c-14.689,0-27.234-5.199-37.641-15.604c-10.402-10.404-15.604-22.949-15.604-37.637 | 8 | c-10.404,10.407-22.949,15.604-37.637,15.604c-14.689,0-27.234-5.199-37.641-15.604c-10.402-10.404-15.604-22.949-15.604-37.637 |
diff --git a/client/src/assets/images/global/tick.svg b/client/src/assets/images/global/tick.html index 230caa111..4784b4807 100644 --- a/client/src/assets/images/global/tick.svg +++ b/client/src/assets/images/global/tick.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | 3 | <g transform="translate(-356.000000, -115.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-356.000000, -115.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="8" transform="translate(356.000000, 115.000000)"> | 4 | <g id="8" transform="translate(356.000000, 115.000000)"> |
7 | <path d="M21,6 L9,18" id="Path-14"></path> | 5 | <path d="M21,6 L9,18" id="Path-14"></path> |
8 | <path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path> | 6 | <path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path> |
diff --git a/client/src/assets/images/global/undo.html b/client/src/assets/images/global/undo.html new file mode 100644 index 000000000..228245c86 --- /dev/null +++ b/client/src/assets/images/global/undo.html | |||
@@ -0,0 +1,9 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-180.000000, -115.000000)" fill="#000000"> | ||
4 | <g id="4" transform="translate(180.000000, 115.000000)"> | ||
5 | <path d="M10,19 C10.5522847,19 11,19.4477153 11,20 C11,20.5522847 10.5522847,21 10,21 C9.99404288,21 9.98809793,20.9999479 9.98216558,20.9998442 C5.01980239,20.990358 1,16.9646166 1,12 C1,7.02943725 5.02943725,3 10,3 C14.9705627,3 19,7.02943725 19,12 L17,12 C17,8.13400675 13.8659932,5 10,5 C6.13400675,5 3,8.13400675 3,12 C3,15.8659932 6.13400675,19 10,19 Z M14,12 L22,12 L18,16 L14,12 Z" id="Combined-Shape" transform="translate(11.500000, 12.000000) scale(-1, 1) translate(-11.500000, -12.000000) "/> | ||
6 | </g> | ||
7 | </g> | ||
8 | </g> | ||
9 | </svg> | ||
diff --git a/client/src/assets/images/global/undo.svg b/client/src/assets/images/global/undo.svg deleted file mode 100644 index f1cca03f7..000000000 --- a/client/src/assets/images/global/undo.svg +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-180.000000, -115.000000)" fill="#000"> | ||
6 | <g id="4" transform="translate(180.000000, 115.000000)"> | ||
7 | <path d="M10,19 C10.5522847,19 11,19.4477153 11,20 C11,20.5522847 10.5522847,21 10,21 C9.99404288,21 9.98809793,20.9999479 9.98216558,20.9998442 C5.01980239,20.990358 1,16.9646166 1,12 C1,7.02943725 5.02943725,3 10,3 C14.9705627,3 19,7.02943725 19,12 L17,12 C17,8.13400675 13.8659932,5 10,5 C6.13400675,5 3,8.13400675 3,12 C3,15.8659932 6.13400675,19 10,19 Z M14,12 L22,12 L18,16 L14,12 Z" id="Combined-Shape" transform="translate(11.500000, 12.000000) scale(-1, 1) translate(-11.500000, -12.000000) "></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/user-add.html b/client/src/assets/images/global/user-add.html new file mode 100644 index 000000000..57df23c74 --- /dev/null +++ b/client/src/assets/images/global/user-add.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-136.000000, -863.000000)"> | ||
4 | <g id="343" transform="translate(136.000000, 863.000000)"> | ||
5 | <path d="M14.2571621,15 L7,15 C4.20063223,15 2.390348,16.1679253 1.5255785,18.0896353 C1.07423388,19.0926234 0.949016905,20.1108713 0.995546634,20.9698816 C0.998604759,21.0263393 1.0014872,21.0632937 1.00496281,21.0995037 C1.0599172,21.6490476 1.54995985,22.0499916 2.09950372,21.9950372 C2.64904758,21.9400828 3.04999158,21.4500401 2.99503719,20.9004963 C2.99555422,20.9071205 2.99399879,20.8871791 2.99261905,20.8617069 C2.96185588,20.2937714 3.05021139,19.575276 3.34942151,18.9103647 C3.890902,17.7070747 4.98686778,17 7,17 L12.0070975,17 L13.2070325,17 C13.4170071,16.2576107 13.7789623,15.5790321 14.2571621,15 Z" id="Path-41" fill="#000000" fill-rule="nonzero"></path> | ||
6 | <path d="M19,18 L19,16.4976988 C19,16.2228273 18.7680664,16 18.5,16 C18.2238576,16 18,16.2148438 18,16.4976988 L18,18 L16.4976988,18 C16.2148438,18 16,18.2238576 16,18.5 C16,18.7680664 16.2228273,19 16.4976988,19 L18,19 L18,20.5023012 C18,20.7771727 18.2319336,21 18.5,21 C18.7761424,21 19,20.7851562 19,20.5023012 L19,19 L20.5023012,19 C20.7851562,19 21,18.7761424 21,18.5 C21,18.2319336 20.7771727,18 20.5023012,18 L19,18 Z M18.5,23 C16.0147186,23 14,20.9852814 14,18.5 C14,16.0147186 16.0147186,14 18.5,14 C20.9852814,14 23,16.0147186 23,18.5 C23,20.9852814 20.9852814,23 18.5,23 Z" id="Combined-Shape" fill="#000000"></path> | ||
7 | <circle id="Oval-40" stroke="#000000" stroke-width="2" cx="12" cy="8" r="5"></circle> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/validate.svg b/client/src/assets/images/global/validate.html index 5c7ee9d14..520624ff6 100644 --- a/client/src/assets/images/global/validate.svg +++ b/client/src/assets/images/global/validate.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-400.000000, -1134.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-400.000000, -1134.000000)" stroke="#ffffff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | 4 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> |
7 | <g id="yes" transform="translate(352.000000, 88.000000)"> | 5 | <g id="yes" transform="translate(352.000000, 88.000000)"> |
8 | <circle id="Oval-1" cx="12" cy="12" r="10"></circle> | 6 | <circle id="Oval-1" cx="12" cy="12" r="10"></circle> |
diff --git a/client/src/assets/images/video/blacklist.svg b/client/src/assets/images/video/blacklist.svg deleted file mode 100644 index 431c73816..000000000 --- a/client/src/assets/images/video/blacklist.svg +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>no</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -863.000000)" stroke="#000000" stroke-width="2"> | ||
9 | <g id="347" transform="translate(312.000000, 863.000000)"> | ||
10 | <circle id="Oval-196" cx="12" cy="12" r="9"></circle> | ||
11 | <path d="M18,18 L6,6" id="Path-275"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/video/dislike-white.svg b/client/src/assets/images/video/dislike-white.svg deleted file mode 100644 index cfc6eaa1f..000000000 --- a/client/src/assets/images/video/dislike-white.svg +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
5 | <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#ffffff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> | ||
8 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> | ||
9 | <path d="M4,4.5 C4,4.5 3,7 3,10 C3,13 4,15.5 4,15.5" id="Path-189" transform="translate(3.500000, 10.000000) scale(1, -1) translate(-3.500000, -10.000000) "></path> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/video/dislike-grey.svg b/client/src/assets/images/video/dislike.html index 56a7908fb..acde951e2 100644 --- a/client/src/assets/images/video/dislike-grey.svg +++ b/client/src/assets/images/video/dislike.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | 3 | <g transform="translate(-752.000000, -1090.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | 4 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> |
7 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> | 5 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> |
8 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> | 6 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> |
diff --git a/client/src/assets/images/video/download-grey.svg b/client/src/assets/images/video/download-grey.svg deleted file mode 100644 index 5b0cca5ef..000000000 --- a/client/src/assets/images/video/download-grey.svg +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>download</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | ||
10 | <path d="M12,3 L12,15" id="Path-58"></path> | ||
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | ||
12 | <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/download-white.svg b/client/src/assets/images/video/download-white.svg deleted file mode 100644 index 0e66e06e8..000000000 --- a/client/src/assets/images/video/download-white.svg +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>download</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#ffffff" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | ||
10 | <path d="M12,3 L12,15" id="Path-58"></path> | ||
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | ||
12 | <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/heart.svg b/client/src/assets/images/video/heart.html index 5d64aee0f..618f64f10 100644 --- a/client/src/assets/images/video/heart.svg +++ b/client/src/assets/images/video/heart.html | |||
@@ -1,9 +1,7 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-48.000000, -1046.000000)" fill-rule="nonzero" fill="#000000"> |
5 | <g id="Artboard-4" transform="translate(-48.000000, -1046.000000)" fill-rule="nonzero" fill="#585858"> | 4 | <g transform="translate(48.000000, 1046.000000)"> |
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="heart"> | 5 | <g id="heart"> |
8 | <path d="M12.0174466,21 L20.9041801,11.3556763 C22.6291961,9.13778099 22.2795957,5.90145416 20.1233257,4.12713796 C17.9670557,2.35282175 14.8206518,2.71241362 13.0956358,4.93030888 L12.0174465,6.5 L10.9043642,4.93030888 C9.17934824,2.71241362 6.0329443,2.35282175 3.87667432,4.12713796 C1.72040435,5.90145416 1.37080391,9.13778099 3.09581989,11.3556763 L12.0174466,21 Z"></path> | 6 | <path d="M12.0174466,21 L20.9041801,11.3556763 C22.6291961,9.13778099 22.2795957,5.90145416 20.1233257,4.12713796 C17.9670557,2.35282175 14.8206518,2.71241362 13.0956358,4.93030888 L12.0174465,6.5 L10.9043642,4.93030888 C9.17934824,2.71241362 6.0329443,2.35282175 3.87667432,4.12713796 C1.72040435,5.90145416 1.37080391,9.13778099 3.09581989,11.3556763 L12.0174466,21 Z"></path> |
9 | </g> | 7 | </g> |
diff --git a/client/src/assets/images/video/like-white.svg b/client/src/assets/images/video/like-white.svg deleted file mode 100644 index 88e5f6a9a..000000000 --- a/client/src/assets/images/video/like-white.svg +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>thumbs-up</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#ffffff" stroke-width="2"> | ||
9 | <g id="256" transform="translate(708.000000, 643.000000)"> | ||
10 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> | ||
11 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/video/like-grey.svg b/client/src/assets/images/video/like.html index 5ef6c7b31..d0e71763b 100644 --- a/client/src/assets/images/video/like-grey.svg +++ b/client/src/assets/images/video/like.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>thumbs-up</title> | 3 | <g transform="translate(-708.000000, -643.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="256" transform="translate(708.000000, 643.000000)"> | 4 | <g id="256" transform="translate(708.000000, 643.000000)"> |
10 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> | 5 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> |
11 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> | 6 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> |
diff --git a/client/src/assets/images/video/more.svg b/client/src/assets/images/video/more.html index dea392136..39dcad10e 100644 --- a/client/src/assets/images/video/more.svg +++ b/client/src/assets/images/video/more.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-444.000000, -115.000000)" fill="#000000"> |
5 | <g id="Artboard-4" transform="translate(-444.000000, -115.000000)" fill="#585858"> | ||
6 | <g id="10" transform="translate(444.000000, 115.000000)"> | 4 | <g id="10" transform="translate(444.000000, 115.000000)"> |
7 | <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path> | 5 | <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path> |
8 | </g> | 6 | </g> |
diff --git a/client/src/assets/images/video/share.svg b/client/src/assets/images/video/share.html index da0f43e81..7759b37af 100644 --- a/client/src/assets/images/video/share.svg +++ b/client/src/assets/images/video/share.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>share</title> | 3 | <g transform="translate(-312.000000, -203.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -203.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="47" transform="translate(312.000000, 203.000000)"> | 4 | <g id="47" transform="translate(312.000000, 203.000000)"> |
10 | <path d="M20,15 L20,18.0026083 C20,19.1057373 19.1073772,20 18.0049107,20 L5.99508929,20 C4.8932319,20 4,19.1073772 4,18.0049107 L4,5.99508929 C4,4.8932319 4.89585781,4 5.9973917,4 L9,4" id="Rectangle-460"></path> | 5 | <path d="M20,15 L20,18.0026083 C20,19.1057373 19.1073772,20 18.0049107,20 L5.99508929,20 C4.8932319,20 4,19.1073772 4,18.0049107 L4,5.99508929 C4,4.8932319 4.89585781,4 5.9973917,4 L9,4" id="Rectangle-460"></path> |
11 | <polyline id="Path-93" stroke-linejoin="round" points="13 4 20.0207973 4 20.0207973 11.0191059"></polyline> | 6 | <polyline id="Path-93" stroke-linejoin="round" points="13 4 20.0207973 4 20.0207973 11.0191059"></polyline> |
diff --git a/client/src/assets/images/header/upload-white.svg b/client/src/assets/images/video/upload.html index 2b07caf76..3bc0d3a8a 100644 --- a/client/src/assets/images/header/upload-white.svg +++ b/client/src/assets/images/video/upload.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>cloud-upload</title> | 3 | <g transform="translate(-312.000000, -775.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#fff" stroke-width="2"> | ||
9 | <g id="307" transform="translate(312.000000, 775.000000)"> | 4 | <g id="307" transform="translate(312.000000, 775.000000)"> |
10 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> | 5 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> |
11 | <path d="M12,13 L12,21" id="Path-58"></path> | 6 | <path d="M12,13 L12,21" id="Path-58"></path> |
diff --git a/client/src/assets/images/video/upload.svg b/client/src/assets/images/video/upload.svg deleted file mode 100644 index c5b7cb443..000000000 --- a/client/src/assets/images/video/upload.svg +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>cloud-upload</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#C6C6C6" stroke-width="2"> | ||
9 | <g id="307" transform="translate(312.000000, 775.000000)"> | ||
10 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> | ||
11 | <path d="M12,13 L12,21" id="Path-58"></path> | ||
12 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 12.500000) scale(1, -1) translate(-12.000000, -12.500000) " points="15 11 12 14 9 11"></polyline> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts new file mode 100644 index 000000000..022a9c16f --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -0,0 +1,143 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
5 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | ||
6 | import { Events } from 'p2p-media-loader-core' | ||
7 | |||
8 | // videojs-hlsjs-plugin needs videojs in window | ||
9 | window['videojs'] = videojs | ||
10 | require('@streamroot/videojs-hlsjs-plugin') | ||
11 | |||
12 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
13 | class P2pMediaLoaderPlugin extends Plugin { | ||
14 | |||
15 | private readonly CONSTANTS = { | ||
16 | INFO_SCHEDULER: 1000 // Don't change this | ||
17 | } | ||
18 | private readonly options: P2PMediaLoaderPluginOptions | ||
19 | |||
20 | private hlsjs: any // Don't type hlsjs to not bundle the module | ||
21 | private p2pEngine: Engine | ||
22 | private statsP2PBytes = { | ||
23 | pendingDownload: [] as number[], | ||
24 | pendingUpload: [] as number[], | ||
25 | numPeers: 0, | ||
26 | totalDownload: 0, | ||
27 | totalUpload: 0 | ||
28 | } | ||
29 | private statsHTTPBytes = { | ||
30 | pendingDownload: [] as number[], | ||
31 | pendingUpload: [] as number[], | ||
32 | totalDownload: 0, | ||
33 | totalUpload: 0 | ||
34 | } | ||
35 | |||
36 | private networkInfoInterval: any | ||
37 | |||
38 | constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { | ||
39 | super(player, options) | ||
40 | |||
41 | this.options = options | ||
42 | |||
43 | videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { | ||
44 | this.hlsjs = hlsjs | ||
45 | }) | ||
46 | |||
47 | initVideoJsContribHlsJsPlayer(player) | ||
48 | |||
49 | player.src({ | ||
50 | type: options.type, | ||
51 | src: options.src | ||
52 | }) | ||
53 | |||
54 | player.on('play', () => { | ||
55 | player.addClass('vjs-has-big-play-button-clicked') | ||
56 | }) | ||
57 | |||
58 | player.ready(() => this.initialize()) | ||
59 | } | ||
60 | |||
61 | dispose () { | ||
62 | if (this.hlsjs) this.hlsjs.destroy() | ||
63 | if (this.p2pEngine) this.p2pEngine.destroy() | ||
64 | |||
65 | clearInterval(this.networkInfoInterval) | ||
66 | } | ||
67 | |||
68 | private initialize () { | ||
69 | initHlsJsPlayer(this.hlsjs) | ||
70 | |||
71 | const tech = this.player.tech_ | ||
72 | this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine() | ||
73 | |||
74 | // Avoid using constants to not import hls.hs | ||
75 | // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 | ||
76 | this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => { | ||
77 | this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) | ||
78 | }) | ||
79 | |||
80 | this.p2pEngine.on(Events.SegmentError, (segment, err) => { | ||
81 | console.error('Segment error.', segment, err) | ||
82 | }) | ||
83 | |||
84 | this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length | ||
85 | |||
86 | this.runStats() | ||
87 | } | ||
88 | |||
89 | private runStats () { | ||
90 | this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { | ||
91 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
92 | |||
93 | elem.pendingDownload.push(size) | ||
94 | elem.totalDownload += size | ||
95 | }) | ||
96 | |||
97 | this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { | ||
98 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
99 | |||
100 | elem.pendingUpload.push(size) | ||
101 | elem.totalUpload += size | ||
102 | }) | ||
103 | |||
104 | this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) | ||
105 | this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) | ||
106 | |||
107 | this.networkInfoInterval = setInterval(() => { | ||
108 | const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) | ||
109 | const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) | ||
110 | |||
111 | const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload) | ||
112 | const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload) | ||
113 | |||
114 | this.statsP2PBytes.pendingDownload = [] | ||
115 | this.statsP2PBytes.pendingUpload = [] | ||
116 | this.statsHTTPBytes.pendingDownload = [] | ||
117 | this.statsHTTPBytes.pendingUpload = [] | ||
118 | |||
119 | return this.player.trigger('p2pInfo', { | ||
120 | http: { | ||
121 | downloadSpeed: httpDownloadSpeed, | ||
122 | uploadSpeed: httpUploadSpeed, | ||
123 | downloaded: this.statsHTTPBytes.totalDownload, | ||
124 | uploaded: this.statsHTTPBytes.totalUpload | ||
125 | }, | ||
126 | p2p: { | ||
127 | downloadSpeed: p2pDownloadSpeed, | ||
128 | uploadSpeed: p2pUploadSpeed, | ||
129 | numPeers: this.statsP2PBytes.numPeers, | ||
130 | downloaded: this.statsP2PBytes.totalDownload, | ||
131 | uploaded: this.statsP2PBytes.totalUpload | ||
132 | } | ||
133 | } as PlayerNetworkInfo) | ||
134 | }, this.CONSTANTS.INFO_SCHEDULER) | ||
135 | } | ||
136 | |||
137 | private arraySum (data: number[]) { | ||
138 | return data.reduce((a: number, b: number) => a + b, 0) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) | ||
143 | export { P2pMediaLoaderPlugin } | ||
diff --git a/client/src/assets/player/p2p-media-loader/segment-url-builder.ts b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts new file mode 100644 index 000000000..32e7ce4f2 --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { basename } from 'path' | ||
2 | import { Segment } from 'p2p-media-loader-core' | ||
3 | |||
4 | function segmentUrlBuilderFactory (baseUrls: string[]) { | ||
5 | return function segmentBuilder (segment: Segment) { | ||
6 | const max = baseUrls.length + 1 | ||
7 | const i = getRandomInt(max) | ||
8 | |||
9 | if (i === max - 1) return segment.url | ||
10 | |||
11 | let newBaseUrl = baseUrls[i] | ||
12 | let middlePart = newBaseUrl.endsWith('/') ? '' : '/' | ||
13 | |||
14 | return newBaseUrl + middlePart + basename(segment.url) | ||
15 | } | ||
16 | } | ||
17 | |||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | export { | ||
21 | segmentUrlBuilderFactory | ||
22 | } | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | function getRandomInt (max: number) { | ||
27 | return Math.floor(Math.random() * Math.floor(max)) | ||
28 | } | ||
diff --git a/client/src/assets/player/p2p-media-loader/segment-validator.ts b/client/src/assets/player/p2p-media-loader/segment-validator.ts new file mode 100644 index 000000000..72c32f9e0 --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/segment-validator.ts | |||
@@ -0,0 +1,63 @@ | |||
1 | import { Segment } from 'p2p-media-loader-core' | ||
2 | import { basename } from 'path' | ||
3 | |||
4 | function segmentValidatorFactory (segmentsSha256Url: string) { | ||
5 | const segmentsJSON = fetchSha256Segments(segmentsSha256Url) | ||
6 | const regex = /bytes=(\d+)-(\d+)/ | ||
7 | |||
8 | return async function segmentValidator (segment: Segment) { | ||
9 | const filename = basename(segment.url) | ||
10 | const captured = regex.exec(segment.range) | ||
11 | |||
12 | const range = captured[1] + '-' + captured[2] | ||
13 | |||
14 | const hashShouldBe = (await segmentsJSON)[filename][range] | ||
15 | if (hashShouldBe === undefined) { | ||
16 | throw new Error(`Unknown segment name ${filename}/${range} in segment validator`) | ||
17 | } | ||
18 | |||
19 | const calculatedSha = bufferToEx(await sha256(segment.data)) | ||
20 | if (calculatedSha !== hashShouldBe) { | ||
21 | throw new Error( | ||
22 | `Hashes does not correspond for segment ${filename}/${range}` + | ||
23 | `(expected: ${hashShouldBe} instead of ${calculatedSha})` | ||
24 | ) | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | export { | ||
32 | segmentValidatorFactory | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | function fetchSha256Segments (url: string) { | ||
38 | return fetch(url) | ||
39 | .then(res => res.json()) | ||
40 | .catch(err => { | ||
41 | console.error('Cannot get sha256 segments', err) | ||
42 | return {} | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | function sha256 (data?: ArrayBuffer) { | ||
47 | if (!data) return undefined | ||
48 | |||
49 | return window.crypto.subtle.digest('SHA-256', data) | ||
50 | } | ||
51 | |||
52 | // Thanks: https://stackoverflow.com/a/53307879 | ||
53 | function bufferToEx (buffer?: ArrayBuffer) { | ||
54 | if (!buffer) return '' | ||
55 | |||
56 | let s = '' | ||
57 | const h = '0123456789abcdef' | ||
58 | const o = new Uint8Array(buffer) | ||
59 | |||
60 | o.forEach((v: any) => s += h[ v >> 4 ] + h[ v & 15 ]) | ||
61 | |||
62 | return s | ||
63 | } | ||
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts new file mode 100644 index 000000000..0ba9bcb11 --- /dev/null +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -0,0 +1,466 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import 'videojs-hotkeys' | ||
5 | import 'videojs-dock' | ||
6 | import 'videojs-contextmenu-ui' | ||
7 | import 'videojs-contrib-quality-levels' | ||
8 | import './peertube-plugin' | ||
9 | import './videojs-components/peertube-link-button' | ||
10 | import './videojs-components/resolution-menu-button' | ||
11 | import './videojs-components/settings-menu-button' | ||
12 | import './videojs-components/p2p-info-button' | ||
13 | import './videojs-components/peertube-load-progress-bar' | ||
14 | import './videojs-components/theater-button' | ||
15 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' | ||
16 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' | ||
17 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | ||
18 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | ||
19 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' | ||
20 | |||
21 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
22 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
23 | // Change Captions to Subtitles/CC | ||
24 | videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' | ||
25 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
26 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
27 | |||
28 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | ||
29 | |||
30 | export type WebtorrentOptions = { | ||
31 | videoFiles: VideoFile[] | ||
32 | } | ||
33 | |||
34 | export type P2PMediaLoaderOptions = { | ||
35 | playlistUrl: string | ||
36 | segmentsSha256Url: string | ||
37 | trackerAnnounce: string[] | ||
38 | redundancyBaseUrls: string[] | ||
39 | videoFiles: VideoFile[] | ||
40 | } | ||
41 | |||
42 | export type CommonOptions = { | ||
43 | playerElement: HTMLVideoElement | ||
44 | onPlayerElementChange: (element: HTMLVideoElement) => void | ||
45 | |||
46 | autoplay: boolean | ||
47 | videoDuration: number | ||
48 | enableHotkeys: boolean | ||
49 | inactivityTimeout: number | ||
50 | poster: string | ||
51 | startTime: number | string | ||
52 | |||
53 | theaterMode: boolean | ||
54 | captions: boolean | ||
55 | peertubeLink: boolean | ||
56 | |||
57 | videoViewUrl: string | ||
58 | embedUrl: string | ||
59 | |||
60 | language?: string | ||
61 | controls?: boolean | ||
62 | muted?: boolean | ||
63 | loop?: boolean | ||
64 | subtitle?: string | ||
65 | |||
66 | videoCaptions: VideoJSCaption[] | ||
67 | |||
68 | userWatching?: UserWatching | ||
69 | |||
70 | serverUrl: string | ||
71 | } | ||
72 | |||
73 | export type PeertubePlayerManagerOptions = { | ||
74 | common: CommonOptions, | ||
75 | webtorrent: WebtorrentOptions, | ||
76 | p2pMediaLoader?: P2PMediaLoaderOptions | ||
77 | } | ||
78 | |||
79 | export class PeertubePlayerManager { | ||
80 | |||
81 | private static videojsLocaleCache: { [ path: string ]: any } = {} | ||
82 | private static playerElementClassName: string | ||
83 | |||
84 | static getServerTranslations (serverUrl: string, locale: string) { | ||
85 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) | ||
86 | // It is the default locale, nothing to translate | ||
87 | if (!path) return Promise.resolve(undefined) | ||
88 | |||
89 | return fetch(path + '/server.json') | ||
90 | .then(res => res.json()) | ||
91 | .catch(err => { | ||
92 | console.error('Cannot get server translations', err) | ||
93 | return undefined | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { | ||
98 | let p2pMediaLoader: any | ||
99 | |||
100 | this.playerElementClassName = options.common.playerElement.className | ||
101 | |||
102 | if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') | ||
103 | if (mode === 'p2p-media-loader') { | ||
104 | [ p2pMediaLoader ] = await Promise.all([ | ||
105 | import('p2p-media-loader-hlsjs'), | ||
106 | import('./p2p-media-loader/p2p-media-loader-plugin') | ||
107 | ]) | ||
108 | } | ||
109 | |||
110 | const videojsOptions = this.getVideojsOptions(mode, options, p2pMediaLoader) | ||
111 | |||
112 | await this.loadLocaleInVideoJS(options.common.serverUrl, options.common.language) | ||
113 | |||
114 | const self = this | ||
115 | return new Promise(res => { | ||
116 | videojs(options.common.playerElement, videojsOptions, function (this: any) { | ||
117 | const player = this | ||
118 | |||
119 | player.tech_.on('error', () => { | ||
120 | // Fallback to webtorrent? | ||
121 | if (mode === 'p2p-media-loader') { | ||
122 | self.fallbackToWebTorrent(player, options) | ||
123 | } | ||
124 | }) | ||
125 | |||
126 | self.addContextMenu(mode, player, options.common.embedUrl) | ||
127 | |||
128 | return res(player) | ||
129 | }) | ||
130 | }) | ||
131 | } | ||
132 | |||
133 | private static async fallbackToWebTorrent (player: any, options: PeertubePlayerManagerOptions) { | ||
134 | const newVideoElement = document.createElement('video') | ||
135 | newVideoElement.className = this.playerElementClassName | ||
136 | |||
137 | // VideoJS wraps our video element inside a div | ||
138 | const currentParentPlayerElement = options.common.playerElement.parentNode | ||
139 | currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement) | ||
140 | |||
141 | options.common.playerElement = newVideoElement | ||
142 | options.common.onPlayerElementChange(newVideoElement) | ||
143 | |||
144 | player.dispose() | ||
145 | |||
146 | await import('./webtorrent/webtorrent-plugin') | ||
147 | |||
148 | const mode = 'webtorrent' | ||
149 | const videojsOptions = this.getVideojsOptions(mode, options) | ||
150 | |||
151 | const self = this | ||
152 | videojs(newVideoElement, videojsOptions, function (this: any) { | ||
153 | const player = this | ||
154 | |||
155 | self.addContextMenu(mode, player, options.common.embedUrl) | ||
156 | }) | ||
157 | } | ||
158 | |||
159 | private static loadLocaleInVideoJS (serverUrl: string, locale: string) { | ||
160 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) | ||
161 | // It is the default locale, nothing to translate | ||
162 | if (!path) return Promise.resolve(undefined) | ||
163 | |||
164 | let p: Promise<any> | ||
165 | |||
166 | if (PeertubePlayerManager.videojsLocaleCache[path]) { | ||
167 | p = Promise.resolve(PeertubePlayerManager.videojsLocaleCache[path]) | ||
168 | } else { | ||
169 | p = fetch(path + '/player.json') | ||
170 | .then(res => res.json()) | ||
171 | .then(json => { | ||
172 | PeertubePlayerManager.videojsLocaleCache[path] = json | ||
173 | return json | ||
174 | }) | ||
175 | .catch(err => { | ||
176 | console.error('Cannot get player translations', err) | ||
177 | return undefined | ||
178 | }) | ||
179 | } | ||
180 | |||
181 | const completeLocale = getCompleteLocale(locale) | ||
182 | return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) | ||
183 | } | ||
184 | |||
185 | private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions, p2pMediaLoaderModule?: any) { | ||
186 | const commonOptions = options.common | ||
187 | const webtorrentOptions = options.webtorrent | ||
188 | const p2pMediaLoaderOptions = options.p2pMediaLoader | ||
189 | |||
190 | let autoplay = options.common.autoplay | ||
191 | let html5 = {} | ||
192 | |||
193 | const plugins: VideoJSPluginOptions = { | ||
194 | peertube: { | ||
195 | mode, | ||
196 | autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | ||
197 | videoViewUrl: commonOptions.videoViewUrl, | ||
198 | videoDuration: commonOptions.videoDuration, | ||
199 | startTime: commonOptions.startTime, | ||
200 | userWatching: commonOptions.userWatching, | ||
201 | subtitle: commonOptions.subtitle, | ||
202 | videoCaptions: commonOptions.videoCaptions | ||
203 | } | ||
204 | } | ||
205 | |||
206 | if (mode === 'p2p-media-loader') { | ||
207 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { | ||
208 | redundancyBaseUrls: options.p2pMediaLoader.redundancyBaseUrls, | ||
209 | type: 'application/x-mpegURL', | ||
210 | src: p2pMediaLoaderOptions.playlistUrl | ||
211 | } | ||
212 | |||
213 | const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce | ||
214 | .filter(t => t.startsWith('ws')) | ||
215 | |||
216 | const p2pMediaLoaderConfig = { | ||
217 | loader: { | ||
218 | trackerAnnounce, | ||
219 | segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url), | ||
220 | rtcConfig: getRtcConfig(), | ||
221 | requiredSegmentsPriority: 5, | ||
222 | segmentUrlBuilder: segmentUrlBuilderFactory(options.p2pMediaLoader.redundancyBaseUrls) | ||
223 | }, | ||
224 | segments: { | ||
225 | swarmId: p2pMediaLoaderOptions.playlistUrl | ||
226 | } | ||
227 | } | ||
228 | const streamrootHls = { | ||
229 | levelLabelHandler: (level: { height: number, width: number }) => { | ||
230 | const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height) | ||
231 | |||
232 | let label = file.resolution.label | ||
233 | if (file.fps >= 50) label += file.fps | ||
234 | |||
235 | return label | ||
236 | }, | ||
237 | html5: { | ||
238 | hlsjsConfig: { | ||
239 | liveSyncDurationCount: 7, | ||
240 | loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() | ||
241 | } | ||
242 | } | ||
243 | } | ||
244 | |||
245 | Object.assign(plugins, { p2pMediaLoader, streamrootHls }) | ||
246 | html5 = streamrootHls.html5 | ||
247 | } | ||
248 | |||
249 | if (mode === 'webtorrent') { | ||
250 | const webtorrent = { | ||
251 | autoplay, | ||
252 | videoDuration: commonOptions.videoDuration, | ||
253 | playerElement: commonOptions.playerElement, | ||
254 | videoFiles: webtorrentOptions.videoFiles | ||
255 | } | ||
256 | Object.assign(plugins, { webtorrent }) | ||
257 | |||
258 | // WebTorrent plugin handles autoplay, because we do some hackish stuff in there | ||
259 | autoplay = false | ||
260 | } | ||
261 | |||
262 | const videojsOptions = { | ||
263 | html5, | ||
264 | |||
265 | // We don't use text track settings for now | ||
266 | textTrackSettings: false, | ||
267 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, | ||
268 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, | ||
269 | |||
270 | muted: commonOptions.muted !== undefined | ||
271 | ? commonOptions.muted | ||
272 | : undefined, // Undefined so the player knows it has to check the local storage | ||
273 | |||
274 | poster: commonOptions.poster, | ||
275 | autoplay: autoplay === true ? 'any' : autoplay, // Use 'any' instead of true to get notifier by videojs if autoplay fails | ||
276 | inactivityTimeout: commonOptions.inactivityTimeout, | ||
277 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | ||
278 | plugins, | ||
279 | controlBar: { | ||
280 | children: this.getControlBarChildren(mode, { | ||
281 | captions: commonOptions.captions, | ||
282 | peertubeLink: commonOptions.peertubeLink, | ||
283 | theaterMode: commonOptions.theaterMode | ||
284 | }) | ||
285 | } | ||
286 | } | ||
287 | |||
288 | if (commonOptions.enableHotkeys === true) { | ||
289 | Object.assign(videojsOptions.plugins, { | ||
290 | hotkeys: { | ||
291 | enableVolumeScroll: false, | ||
292 | enableModifiersForNumbers: false, | ||
293 | |||
294 | fullscreenKey: function (event: KeyboardEvent) { | ||
295 | // fullscreen with the f key or Ctrl+Enter | ||
296 | return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') | ||
297 | }, | ||
298 | |||
299 | seekStep: function (event: KeyboardEvent) { | ||
300 | // mimic VLC seek behavior, and default to 5 (original value is 5). | ||
301 | if (event.ctrlKey && event.altKey) { | ||
302 | return 5 * 60 | ||
303 | } else if (event.ctrlKey) { | ||
304 | return 60 | ||
305 | } else if (event.altKey) { | ||
306 | return 10 | ||
307 | } else { | ||
308 | return 5 | ||
309 | } | ||
310 | }, | ||
311 | |||
312 | customKeys: { | ||
313 | increasePlaybackRateKey: { | ||
314 | key: function (event: KeyboardEvent) { | ||
315 | return event.key === '>' | ||
316 | }, | ||
317 | handler: function (player: videojs.Player) { | ||
318 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | ||
319 | } | ||
320 | }, | ||
321 | decreasePlaybackRateKey: { | ||
322 | key: function (event: KeyboardEvent) { | ||
323 | return event.key === '<' | ||
324 | }, | ||
325 | handler: function (player: videojs.Player) { | ||
326 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | ||
327 | } | ||
328 | }, | ||
329 | frameByFrame: { | ||
330 | key: function (event: KeyboardEvent) { | ||
331 | return event.key === '.' | ||
332 | }, | ||
333 | handler: function (player: videojs.Player) { | ||
334 | player.pause() | ||
335 | // Calculate movement distance (assuming 30 fps) | ||
336 | const dist = 1 / 30 | ||
337 | player.currentTime(player.currentTime() + dist) | ||
338 | } | ||
339 | } | ||
340 | } | ||
341 | } | ||
342 | }) | ||
343 | } | ||
344 | |||
345 | if (commonOptions.language && !isDefaultLocale(commonOptions.language)) { | ||
346 | Object.assign(videojsOptions, { language: commonOptions.language }) | ||
347 | } | ||
348 | |||
349 | return videojsOptions | ||
350 | } | ||
351 | |||
352 | private static getControlBarChildren (mode: PlayerMode, options: { | ||
353 | peertubeLink: boolean | ||
354 | theaterMode: boolean, | ||
355 | captions: boolean | ||
356 | }) { | ||
357 | const settingEntries = [] | ||
358 | const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar' | ||
359 | |||
360 | // Keep an order | ||
361 | settingEntries.push('playbackRateMenuButton') | ||
362 | if (options.captions === true) settingEntries.push('captionsButton') | ||
363 | settingEntries.push('resolutionMenuButton') | ||
364 | |||
365 | const children = { | ||
366 | 'playToggle': {}, | ||
367 | 'currentTimeDisplay': {}, | ||
368 | 'timeDivider': {}, | ||
369 | 'durationDisplay': {}, | ||
370 | 'liveDisplay': {}, | ||
371 | |||
372 | 'flexibleWidthSpacer': {}, | ||
373 | 'progressControl': { | ||
374 | children: { | ||
375 | 'seekBar': { | ||
376 | children: { | ||
377 | [loadProgressBar]: {}, | ||
378 | 'mouseTimeDisplay': {}, | ||
379 | 'playProgressBar': {} | ||
380 | } | ||
381 | } | ||
382 | } | ||
383 | }, | ||
384 | |||
385 | 'p2PInfoButton': {}, | ||
386 | |||
387 | 'muteToggle': {}, | ||
388 | 'volumeControl': {}, | ||
389 | |||
390 | 'settingsButton': { | ||
391 | setup: { | ||
392 | maxHeightOffset: 40 | ||
393 | }, | ||
394 | entries: settingEntries | ||
395 | } | ||
396 | } | ||
397 | |||
398 | if (options.peertubeLink === true) { | ||
399 | Object.assign(children, { | ||
400 | 'peerTubeLinkButton': {} | ||
401 | }) | ||
402 | } | ||
403 | |||
404 | if (options.theaterMode === true) { | ||
405 | Object.assign(children, { | ||
406 | 'theaterButton': {} | ||
407 | }) | ||
408 | } | ||
409 | |||
410 | Object.assign(children, { | ||
411 | 'fullscreenToggle': {} | ||
412 | }) | ||
413 | |||
414 | return children | ||
415 | } | ||
416 | |||
417 | private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) { | ||
418 | const content = [ | ||
419 | { | ||
420 | label: player.localize('Copy the video URL'), | ||
421 | listener: function () { | ||
422 | copyToClipboard(buildVideoLink()) | ||
423 | } | ||
424 | }, | ||
425 | { | ||
426 | label: player.localize('Copy the video URL at the current time'), | ||
427 | listener: function () { | ||
428 | const player = this as videojs.Player | ||
429 | copyToClipboard(buildVideoLink(player.currentTime())) | ||
430 | } | ||
431 | }, | ||
432 | { | ||
433 | label: player.localize('Copy embed code'), | ||
434 | listener: () => { | ||
435 | copyToClipboard(buildVideoEmbed(videoEmbedUrl)) | ||
436 | } | ||
437 | } | ||
438 | ] | ||
439 | |||
440 | if (mode === 'webtorrent') { | ||
441 | content.push({ | ||
442 | label: player.localize('Copy magnet URI'), | ||
443 | listener: function () { | ||
444 | const player = this as videojs.Player | ||
445 | copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri) | ||
446 | } | ||
447 | }) | ||
448 | } | ||
449 | |||
450 | player.contextmenuUI({ content }) | ||
451 | } | ||
452 | |||
453 | private static getLocalePath (serverUrl: string, locale: string) { | ||
454 | const completeLocale = getCompleteLocale(locale) | ||
455 | |||
456 | if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined | ||
457 | |||
458 | return serverUrl + '/client/locales/' + completeLocale | ||
459 | } | ||
460 | } | ||
461 | |||
462 | // ############################################################################ | ||
463 | |||
464 | export { | ||
465 | videojs | ||
466 | } | ||
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts deleted file mode 100644 index 2de6d7fef..000000000 --- a/client/src/assets/player/peertube-player.ts +++ /dev/null | |||
@@ -1,300 +0,0 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | |||
3 | import 'videojs-hotkeys' | ||
4 | import 'videojs-dock' | ||
5 | import 'videojs-contextmenu-ui' | ||
6 | import './peertube-link-button' | ||
7 | import './resolution-menu-button' | ||
8 | import './settings-menu-button' | ||
9 | import './webtorrent-info-button' | ||
10 | import './peertube-videojs-plugin' | ||
11 | import './peertube-load-progress-bar' | ||
12 | import './theater-button' | ||
13 | import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings' | ||
14 | import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' | ||
15 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | ||
16 | |||
17 | // FIXME: something weird with our path definition in tsconfig and typings | ||
18 | // @ts-ignore | ||
19 | import { Player } from 'video.js' | ||
20 | |||
21 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
22 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
23 | // Change Captions to Subtitles/CC | ||
24 | videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' | ||
25 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
26 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
27 | |||
28 | function getVideojsOptions (options: { | ||
29 | autoplay: boolean | ||
30 | playerElement: HTMLVideoElement | ||
31 | videoViewUrl: string | ||
32 | videoDuration: number | ||
33 | videoFiles: VideoFile[] | ||
34 | enableHotkeys: boolean | ||
35 | inactivityTimeout: number | ||
36 | peertubeLink: boolean | ||
37 | poster: string | ||
38 | startTime: number | string | ||
39 | theaterMode: boolean | ||
40 | videoCaptions: VideoJSCaption[] | ||
41 | |||
42 | language?: string | ||
43 | controls?: boolean | ||
44 | muted?: boolean | ||
45 | loop?: boolean | ||
46 | subtitle?: string | ||
47 | |||
48 | userWatching?: UserWatching | ||
49 | }) { | ||
50 | const videojsOptions = { | ||
51 | // We don't use text track settings for now | ||
52 | textTrackSettings: false, | ||
53 | controls: options.controls !== undefined ? options.controls : true, | ||
54 | loop: options.loop !== undefined ? options.loop : false, | ||
55 | |||
56 | muted: options.muted !== undefined ? options.muted : undefined, // Undefined so the player knows it has to check the local storage | ||
57 | |||
58 | poster: options.poster, | ||
59 | autoplay: false, | ||
60 | inactivityTimeout: options.inactivityTimeout, | ||
61 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | ||
62 | plugins: { | ||
63 | peertube: { | ||
64 | autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | ||
65 | videoCaptions: options.videoCaptions, | ||
66 | videoFiles: options.videoFiles, | ||
67 | playerElement: options.playerElement, | ||
68 | videoViewUrl: options.videoViewUrl, | ||
69 | videoDuration: options.videoDuration, | ||
70 | startTime: options.startTime, | ||
71 | userWatching: options.userWatching, | ||
72 | subtitle: options.subtitle | ||
73 | } | ||
74 | }, | ||
75 | controlBar: { | ||
76 | children: getControlBarChildren(options) | ||
77 | } | ||
78 | } | ||
79 | |||
80 | if (options.enableHotkeys === true) { | ||
81 | Object.assign(videojsOptions.plugins, { | ||
82 | hotkeys: { | ||
83 | enableVolumeScroll: false, | ||
84 | enableModifiersForNumbers: false, | ||
85 | |||
86 | fullscreenKey: function (event: KeyboardEvent) { | ||
87 | // fullscreen with the f key or Ctrl+Enter | ||
88 | return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') | ||
89 | }, | ||
90 | |||
91 | seekStep: function (event: KeyboardEvent) { | ||
92 | // mimic VLC seek behavior, and default to 5 (original value is 5). | ||
93 | if (event.ctrlKey && event.altKey) { | ||
94 | return 5 * 60 | ||
95 | } else if (event.ctrlKey) { | ||
96 | return 60 | ||
97 | } else if (event.altKey) { | ||
98 | return 10 | ||
99 | } else { | ||
100 | return 5 | ||
101 | } | ||
102 | }, | ||
103 | |||
104 | customKeys: { | ||
105 | increasePlaybackRateKey: { | ||
106 | key: function (event: KeyboardEvent) { | ||
107 | return event.key === '>' | ||
108 | }, | ||
109 | handler: function (player: Player) { | ||
110 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | ||
111 | } | ||
112 | }, | ||
113 | decreasePlaybackRateKey: { | ||
114 | key: function (event: KeyboardEvent) { | ||
115 | return event.key === '<' | ||
116 | }, | ||
117 | handler: function (player: Player) { | ||
118 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | ||
119 | } | ||
120 | }, | ||
121 | frameByFrame: { | ||
122 | key: function (event: KeyboardEvent) { | ||
123 | return event.key === '.' | ||
124 | }, | ||
125 | handler: function (player: Player) { | ||
126 | player.pause() | ||
127 | // Calculate movement distance (assuming 30 fps) | ||
128 | const dist = 1 / 30 | ||
129 | player.currentTime(player.currentTime() + dist) | ||
130 | } | ||
131 | } | ||
132 | } | ||
133 | } | ||
134 | }) | ||
135 | } | ||
136 | |||
137 | if (options.language && !isDefaultLocale(options.language)) { | ||
138 | Object.assign(videojsOptions, { language: options.language }) | ||
139 | } | ||
140 | |||
141 | return videojsOptions | ||
142 | } | ||
143 | |||
144 | function getControlBarChildren (options: { | ||
145 | peertubeLink: boolean | ||
146 | theaterMode: boolean, | ||
147 | videoCaptions: VideoJSCaption[] | ||
148 | }) { | ||
149 | const settingEntries = [] | ||
150 | |||
151 | // Keep an order | ||
152 | settingEntries.push('playbackRateMenuButton') | ||
153 | if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton') | ||
154 | settingEntries.push('resolutionMenuButton') | ||
155 | |||
156 | const children = { | ||
157 | 'playToggle': {}, | ||
158 | 'currentTimeDisplay': {}, | ||
159 | 'timeDivider': {}, | ||
160 | 'durationDisplay': {}, | ||
161 | 'liveDisplay': {}, | ||
162 | |||
163 | 'flexibleWidthSpacer': {}, | ||
164 | 'progressControl': { | ||
165 | children: { | ||
166 | 'seekBar': { | ||
167 | children: { | ||
168 | 'peerTubeLoadProgressBar': {}, | ||
169 | 'mouseTimeDisplay': {}, | ||
170 | 'playProgressBar': {} | ||
171 | } | ||
172 | } | ||
173 | } | ||
174 | }, | ||
175 | |||
176 | 'webTorrentButton': {}, | ||
177 | |||
178 | 'muteToggle': {}, | ||
179 | 'volumeControl': {}, | ||
180 | |||
181 | 'settingsButton': { | ||
182 | setup: { | ||
183 | maxHeightOffset: 40 | ||
184 | }, | ||
185 | entries: settingEntries | ||
186 | } | ||
187 | } | ||
188 | |||
189 | if (options.peertubeLink === true) { | ||
190 | Object.assign(children, { | ||
191 | 'peerTubeLinkButton': {} | ||
192 | }) | ||
193 | } | ||
194 | |||
195 | if (options.theaterMode === true) { | ||
196 | Object.assign(children, { | ||
197 | 'theaterButton': {} | ||
198 | }) | ||
199 | } | ||
200 | |||
201 | Object.assign(children, { | ||
202 | 'fullscreenToggle': {} | ||
203 | }) | ||
204 | |||
205 | return children | ||
206 | } | ||
207 | |||
208 | function addContextMenu (player: any, videoEmbedUrl: string) { | ||
209 | player.contextmenuUI({ | ||
210 | content: [ | ||
211 | { | ||
212 | label: player.localize('Copy the video URL'), | ||
213 | listener: function () { | ||
214 | copyToClipboard(buildVideoLink()) | ||
215 | } | ||
216 | }, | ||
217 | { | ||
218 | label: player.localize('Copy the video URL at the current time'), | ||
219 | listener: function () { | ||
220 | const player = this as Player | ||
221 | copyToClipboard(buildVideoLink(player.currentTime())) | ||
222 | } | ||
223 | }, | ||
224 | { | ||
225 | label: player.localize('Copy embed code'), | ||
226 | listener: () => { | ||
227 | copyToClipboard(buildVideoEmbed(videoEmbedUrl)) | ||
228 | } | ||
229 | }, | ||
230 | { | ||
231 | label: player.localize('Copy magnet URI'), | ||
232 | listener: function () { | ||
233 | const player = this as Player | ||
234 | copyToClipboard(player.peertube().getCurrentVideoFile().magnetUri) | ||
235 | } | ||
236 | } | ||
237 | ] | ||
238 | }) | ||
239 | } | ||
240 | |||
241 | function loadLocaleInVideoJS (serverUrl: string, videojs: any, locale: string) { | ||
242 | const path = getLocalePath(serverUrl, locale) | ||
243 | // It is the default locale, nothing to translate | ||
244 | if (!path) return Promise.resolve(undefined) | ||
245 | |||
246 | let p: Promise<any> | ||
247 | |||
248 | if (loadLocaleInVideoJS.cache[path]) { | ||
249 | p = Promise.resolve(loadLocaleInVideoJS.cache[path]) | ||
250 | } else { | ||
251 | p = fetch(path + '/player.json') | ||
252 | .then(res => res.json()) | ||
253 | .then(json => { | ||
254 | loadLocaleInVideoJS.cache[path] = json | ||
255 | return json | ||
256 | }) | ||
257 | .catch(err => { | ||
258 | console.error('Cannot get player translations', err) | ||
259 | return undefined | ||
260 | }) | ||
261 | } | ||
262 | |||
263 | const completeLocale = getCompleteLocale(locale) | ||
264 | return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) | ||
265 | } | ||
266 | namespace loadLocaleInVideoJS { | ||
267 | export const cache: { [ path: string ]: any } = {} | ||
268 | } | ||
269 | |||
270 | function getServerTranslations (serverUrl: string, locale: string) { | ||
271 | const path = getLocalePath(serverUrl, locale) | ||
272 | // It is the default locale, nothing to translate | ||
273 | if (!path) return Promise.resolve(undefined) | ||
274 | |||
275 | return fetch(path + '/server.json') | ||
276 | .then(res => res.json()) | ||
277 | .catch(err => { | ||
278 | console.error('Cannot get server translations', err) | ||
279 | return undefined | ||
280 | }) | ||
281 | } | ||
282 | |||
283 | // ############################################################################ | ||
284 | |||
285 | export { | ||
286 | getServerTranslations, | ||
287 | loadLocaleInVideoJS, | ||
288 | getVideojsOptions, | ||
289 | addContextMenu | ||
290 | } | ||
291 | |||
292 | // ############################################################################ | ||
293 | |||
294 | function getLocalePath (serverUrl: string, locale: string) { | ||
295 | const completeLocale = getCompleteLocale(locale) | ||
296 | |||
297 | if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined | ||
298 | |||
299 | return serverUrl + '/client/locales/' + completeLocale | ||
300 | } | ||
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts new file mode 100644 index 000000000..7ea4a06d4 --- /dev/null +++ b/client/src/assets/player/peertube-plugin.ts | |||
@@ -0,0 +1,262 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import './videojs-components/settings-menu-button' | ||
5 | import { | ||
6 | PeerTubePluginOptions, | ||
7 | ResolutionUpdateData, | ||
8 | UserWatching, | ||
9 | VideoJSCaption, | ||
10 | VideoJSComponentInterface, | ||
11 | videojsUntyped | ||
12 | } from './peertube-videojs-typings' | ||
13 | import { isMobile, timeToInt } from './utils' | ||
14 | import { | ||
15 | getStoredLastSubtitle, | ||
16 | getStoredMute, | ||
17 | getStoredVolume, | ||
18 | saveLastSubtitle, | ||
19 | saveMuteInStore, | ||
20 | saveVolumeInStore | ||
21 | } from './peertube-player-local-storage' | ||
22 | |||
23 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
24 | class PeerTubePlugin extends Plugin { | ||
25 | private readonly autoplay: boolean = false | ||
26 | private readonly startTime: number = 0 | ||
27 | private readonly videoViewUrl: string | ||
28 | private readonly videoDuration: number | ||
29 | private readonly CONSTANTS = { | ||
30 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | ||
31 | } | ||
32 | |||
33 | private player: any | ||
34 | private videoCaptions: VideoJSCaption[] | ||
35 | private defaultSubtitle: string | ||
36 | |||
37 | private videoViewInterval: any | ||
38 | private userWatchingVideoInterval: any | ||
39 | private qualityObservationTimer: any | ||
40 | private lastResolutionChange: ResolutionUpdateData | ||
41 | |||
42 | constructor (player: videojs.Player, options: PeerTubePluginOptions) { | ||
43 | super(player, options) | ||
44 | |||
45 | this.startTime = timeToInt(options.startTime) | ||
46 | this.videoViewUrl = options.videoViewUrl | ||
47 | this.videoDuration = options.videoDuration | ||
48 | this.videoCaptions = options.videoCaptions | ||
49 | |||
50 | if (options.autoplay === true) this.player.addClass('vjs-has-autoplay') | ||
51 | |||
52 | this.player.on('autoplay-failure', () => { | ||
53 | this.player.removeClass('vjs-has-autoplay') | ||
54 | }) | ||
55 | |||
56 | this.player.ready(() => { | ||
57 | const playerOptions = this.player.options_ | ||
58 | |||
59 | if (options.mode === 'webtorrent') { | ||
60 | this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | ||
61 | this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d)) | ||
62 | } | ||
63 | |||
64 | if (options.mode === 'p2p-media-loader') { | ||
65 | this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | ||
66 | } | ||
67 | |||
68 | this.player.tech_.on('loadedqualitydata', () => { | ||
69 | setTimeout(() => { | ||
70 | // Replay a resolution change, now we loaded all quality data | ||
71 | if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange) | ||
72 | }, 0) | ||
73 | }) | ||
74 | |||
75 | const volume = getStoredVolume() | ||
76 | if (volume !== undefined) this.player.volume(volume) | ||
77 | |||
78 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | ||
79 | if (muted !== undefined) this.player.muted(muted) | ||
80 | |||
81 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | ||
82 | |||
83 | this.player.on('volumechange', () => { | ||
84 | saveVolumeInStore(this.player.volume()) | ||
85 | saveMuteInStore(this.player.muted()) | ||
86 | }) | ||
87 | |||
88 | this.player.textTracks().on('change', () => { | ||
89 | const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { | ||
90 | return t.kind === 'captions' && t.mode === 'showing' | ||
91 | }) | ||
92 | |||
93 | if (!showing) { | ||
94 | saveLastSubtitle('off') | ||
95 | return | ||
96 | } | ||
97 | |||
98 | saveLastSubtitle(showing.language) | ||
99 | }) | ||
100 | |||
101 | this.player.on('sourcechange', () => this.initCaptions()) | ||
102 | |||
103 | this.player.duration(options.videoDuration) | ||
104 | |||
105 | this.initializePlayer() | ||
106 | this.runViewAdd() | ||
107 | |||
108 | if (options.userWatching) this.runUserWatchVideo(options.userWatching) | ||
109 | }) | ||
110 | } | ||
111 | |||
112 | dispose () { | ||
113 | clearTimeout(this.qualityObservationTimer) | ||
114 | |||
115 | clearInterval(this.videoViewInterval) | ||
116 | |||
117 | if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) | ||
118 | } | ||
119 | |||
120 | private initializePlayer () { | ||
121 | if (isMobile()) this.player.addClass('vjs-is-mobile') | ||
122 | |||
123 | this.initSmoothProgressBar() | ||
124 | |||
125 | this.initCaptions() | ||
126 | |||
127 | this.alterInactivity() | ||
128 | } | ||
129 | |||
130 | private runViewAdd () { | ||
131 | this.clearVideoViewInterval() | ||
132 | |||
133 | // After 30 seconds (or 3/4 of the video), add a view to the video | ||
134 | let minSecondsToView = 30 | ||
135 | |||
136 | if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 | ||
137 | |||
138 | let secondsViewed = 0 | ||
139 | this.videoViewInterval = setInterval(() => { | ||
140 | if (this.player && !this.player.paused()) { | ||
141 | secondsViewed += 1 | ||
142 | |||
143 | if (secondsViewed > minSecondsToView) { | ||
144 | this.clearVideoViewInterval() | ||
145 | |||
146 | this.addViewToVideo().catch(err => console.error(err)) | ||
147 | } | ||
148 | } | ||
149 | }, 1000) | ||
150 | } | ||
151 | |||
152 | private runUserWatchVideo (options: UserWatching) { | ||
153 | let lastCurrentTime = 0 | ||
154 | |||
155 | this.userWatchingVideoInterval = setInterval(() => { | ||
156 | const currentTime = Math.floor(this.player.currentTime()) | ||
157 | |||
158 | if (currentTime - lastCurrentTime >= 1) { | ||
159 | lastCurrentTime = currentTime | ||
160 | |||
161 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | ||
162 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
163 | } | ||
164 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | ||
165 | } | ||
166 | |||
167 | private clearVideoViewInterval () { | ||
168 | if (this.videoViewInterval !== undefined) { | ||
169 | clearInterval(this.videoViewInterval) | ||
170 | this.videoViewInterval = undefined | ||
171 | } | ||
172 | } | ||
173 | |||
174 | private addViewToVideo () { | ||
175 | if (!this.videoViewUrl) return Promise.resolve(undefined) | ||
176 | |||
177 | return fetch(this.videoViewUrl, { method: 'POST' }) | ||
178 | } | ||
179 | |||
180 | private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { | ||
181 | const body = new URLSearchParams() | ||
182 | body.append('currentTime', currentTime.toString()) | ||
183 | |||
184 | const headers = new Headers({ 'Authorization': authorizationHeader }) | ||
185 | |||
186 | return fetch(url, { method: 'PUT', body, headers }) | ||
187 | } | ||
188 | |||
189 | private handleResolutionChange (data: ResolutionUpdateData) { | ||
190 | this.lastResolutionChange = data | ||
191 | |||
192 | const qualityLevels = this.player.qualityLevels() | ||
193 | |||
194 | for (let i = 0; i < qualityLevels.length; i++) { | ||
195 | if (qualityLevels[i].height === data.resolutionId) { | ||
196 | data.id = qualityLevels[i].id | ||
197 | break | ||
198 | } | ||
199 | } | ||
200 | |||
201 | this.trigger('resolutionChange', data) | ||
202 | } | ||
203 | |||
204 | private alterInactivity () { | ||
205 | let saveInactivityTimeout: number | ||
206 | |||
207 | const disableInactivity = () => { | ||
208 | saveInactivityTimeout = this.player.options_.inactivityTimeout | ||
209 | this.player.options_.inactivityTimeout = 0 | ||
210 | } | ||
211 | const enableInactivity = () => { | ||
212 | this.player.options_.inactivityTimeout = saveInactivityTimeout | ||
213 | } | ||
214 | |||
215 | const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog') | ||
216 | |||
217 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | ||
218 | settingsDialog.on('mouseenter', () => disableInactivity()) | ||
219 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | ||
220 | settingsDialog.on('mouseleave', () => enableInactivity()) | ||
221 | } | ||
222 | |||
223 | private initCaptions () { | ||
224 | for (const caption of this.videoCaptions) { | ||
225 | this.player.addRemoteTextTrack({ | ||
226 | kind: 'captions', | ||
227 | label: caption.label, | ||
228 | language: caption.language, | ||
229 | id: caption.language, | ||
230 | src: caption.src, | ||
231 | default: this.defaultSubtitle === caption.language | ||
232 | }, false) | ||
233 | } | ||
234 | |||
235 | this.player.trigger('captionsChanged') | ||
236 | } | ||
237 | |||
238 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | ||
239 | private initSmoothProgressBar () { | ||
240 | const SeekBar = videojsUntyped.getComponent('SeekBar') | ||
241 | SeekBar.prototype.getPercent = function getPercent () { | ||
242 | // Allows for smooth scrubbing, when player can't keep up. | ||
243 | // const time = (this.player_.scrubbing()) ? | ||
244 | // this.player_.getCache().currentTime : | ||
245 | // this.player_.currentTime() | ||
246 | const time = this.player_.currentTime() | ||
247 | const percent = time / this.player_.duration() | ||
248 | return percent >= 1 ? 1 : percent | ||
249 | } | ||
250 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { | ||
251 | let newTime = this.calculateDistance(event) * this.player_.duration() | ||
252 | if (newTime === this.player_.duration()) { | ||
253 | newTime = newTime - 0.1 | ||
254 | } | ||
255 | this.player_.currentTime(newTime) | ||
256 | this.update() | ||
257 | } | ||
258 | } | ||
259 | } | ||
260 | |||
261 | videojs.registerPlugin('peertube', PeerTubePlugin) | ||
262 | export { PeerTubePlugin } | ||
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index 634c7fdc9..79a5a6c4d 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -3,11 +3,16 @@ | |||
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 5 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
6 | import { PeerTubePlugin } from './peertube-videojs-plugin' | 6 | import { PeerTubePlugin } from './peertube-plugin' |
7 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | ||
8 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | ||
9 | import { PlayerMode } from './peertube-player-manager' | ||
7 | 10 | ||
8 | declare namespace videojs { | 11 | declare namespace videojs { |
9 | interface Player { | 12 | interface Player { |
10 | peertube (): PeerTubePlugin | 13 | peertube (): PeerTubePlugin |
14 | webtorrent (): WebTorrentPlugin | ||
15 | p2pMediaLoader (): P2pMediaLoaderPlugin | ||
11 | } | 16 | } |
12 | } | 17 | } |
13 | 18 | ||
@@ -30,26 +35,95 @@ type UserWatching = { | |||
30 | authorizationHeader: string | 35 | authorizationHeader: string |
31 | } | 36 | } |
32 | 37 | ||
33 | type PeertubePluginOptions = { | 38 | type PeerTubePluginOptions = { |
34 | videoFiles: VideoFile[] | 39 | mode: PlayerMode |
35 | playerElement: HTMLVideoElement | 40 | |
41 | autoplay: boolean | ||
36 | videoViewUrl: string | 42 | videoViewUrl: string |
37 | videoDuration: number | 43 | videoDuration: number |
38 | startTime: number | string | 44 | startTime: number | string |
39 | autoplay: boolean, | ||
40 | videoCaptions: VideoJSCaption[] | ||
41 | 45 | ||
42 | subtitle?: string | ||
43 | userWatching?: UserWatching | 46 | userWatching?: UserWatching |
47 | subtitle?: string | ||
48 | |||
49 | videoCaptions: VideoJSCaption[] | ||
50 | } | ||
51 | |||
52 | type WebtorrentPluginOptions = { | ||
53 | playerElement: HTMLVideoElement | ||
54 | |||
55 | autoplay: boolean | ||
56 | videoDuration: number | ||
57 | |||
58 | videoFiles: VideoFile[] | ||
59 | } | ||
60 | |||
61 | type P2PMediaLoaderPluginOptions = { | ||
62 | redundancyBaseUrls: string[] | ||
63 | type: string | ||
64 | src: string | ||
65 | } | ||
66 | |||
67 | type VideoJSPluginOptions = { | ||
68 | peertube: PeerTubePluginOptions | ||
69 | |||
70 | webtorrent?: WebtorrentPluginOptions | ||
71 | |||
72 | p2pMediaLoader?: P2PMediaLoaderPluginOptions | ||
44 | } | 73 | } |
45 | 74 | ||
46 | // videojs typings don't have some method we need | 75 | // videojs typings don't have some method we need |
47 | const videojsUntyped = videojs as any | 76 | const videojsUntyped = videojs as any |
48 | 77 | ||
78 | type LoadedQualityData = { | ||
79 | qualitySwitchCallback: Function, | ||
80 | qualityData: { | ||
81 | video: { | ||
82 | id: number | ||
83 | label: string | ||
84 | selected: boolean | ||
85 | }[] | ||
86 | } | ||
87 | } | ||
88 | |||
89 | type ResolutionUpdateData = { | ||
90 | auto: boolean, | ||
91 | resolutionId: number | ||
92 | id?: number | ||
93 | } | ||
94 | |||
95 | type AutoResolutionUpdateData = { | ||
96 | possible: boolean | ||
97 | } | ||
98 | |||
99 | type PlayerNetworkInfo = { | ||
100 | http: { | ||
101 | downloadSpeed: number | ||
102 | uploadSpeed: number | ||
103 | downloaded: number | ||
104 | uploaded: number | ||
105 | } | ||
106 | |||
107 | p2p: { | ||
108 | downloadSpeed: number | ||
109 | uploadSpeed: number | ||
110 | downloaded: number | ||
111 | uploaded: number | ||
112 | numPeers: number | ||
113 | } | ||
114 | } | ||
115 | |||
49 | export { | 116 | export { |
117 | PlayerNetworkInfo, | ||
118 | ResolutionUpdateData, | ||
119 | AutoResolutionUpdateData, | ||
50 | VideoJSComponentInterface, | 120 | VideoJSComponentInterface, |
51 | PeertubePluginOptions, | ||
52 | videojsUntyped, | 121 | videojsUntyped, |
53 | VideoJSCaption, | 122 | VideoJSCaption, |
54 | UserWatching | 123 | UserWatching, |
124 | PeerTubePluginOptions, | ||
125 | WebtorrentPluginOptions, | ||
126 | P2PMediaLoaderPluginOptions, | ||
127 | VideoJSPluginOptions, | ||
128 | LoadedQualityData | ||
55 | } | 129 | } |
diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/resolution-menu-button.ts deleted file mode 100644 index a3c1108ca..000000000 --- a/client/src/assets/player/resolution-menu-button.ts +++ /dev/null | |||
@@ -1,88 +0,0 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
6 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
7 | |||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | ||
9 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
10 | class ResolutionMenuButton extends MenuButton { | ||
11 | label: HTMLElement | ||
12 | |||
13 | constructor (player: Player, options: any) { | ||
14 | super(player, options) | ||
15 | this.player = player | ||
16 | |||
17 | player.peertube().on('videoFileUpdate', () => this.updateLabel()) | ||
18 | player.peertube().on('autoResolutionUpdate', () => this.updateLabel()) | ||
19 | } | ||
20 | |||
21 | createEl () { | ||
22 | const el = super.createEl() | ||
23 | |||
24 | this.labelEl_ = videojsUntyped.dom.createEl('div', { | ||
25 | className: 'vjs-resolution-value', | ||
26 | innerHTML: this.buildLabelHTML() | ||
27 | }) | ||
28 | |||
29 | el.appendChild(this.labelEl_) | ||
30 | |||
31 | return el | ||
32 | } | ||
33 | |||
34 | updateARIAAttributes () { | ||
35 | this.el().setAttribute('aria-label', 'Quality') | ||
36 | } | ||
37 | |||
38 | createMenu () { | ||
39 | const menu = new Menu(this.player_) | ||
40 | for (const videoFile of this.player_.peertube().videoFiles) { | ||
41 | let label = videoFile.resolution.label | ||
42 | if (videoFile.fps && videoFile.fps >= 50) { | ||
43 | label += videoFile.fps | ||
44 | } | ||
45 | |||
46 | menu.addChild(new ResolutionMenuItem( | ||
47 | this.player_, | ||
48 | { | ||
49 | id: videoFile.resolution.id, | ||
50 | label, | ||
51 | src: videoFile.magnetUri | ||
52 | }) | ||
53 | ) | ||
54 | } | ||
55 | |||
56 | menu.addChild(new ResolutionMenuItem( | ||
57 | this.player_, | ||
58 | { | ||
59 | id: -1, | ||
60 | label: this.player_.localize('Auto'), | ||
61 | src: null | ||
62 | } | ||
63 | )) | ||
64 | |||
65 | return menu | ||
66 | } | ||
67 | |||
68 | updateLabel () { | ||
69 | if (!this.labelEl_) return | ||
70 | |||
71 | this.labelEl_.innerHTML = this.buildLabelHTML() | ||
72 | } | ||
73 | |||
74 | buildCSSClass () { | ||
75 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
76 | } | ||
77 | |||
78 | buildWrapperCSSClass () { | ||
79 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
80 | } | ||
81 | |||
82 | private buildLabelHTML () { | ||
83 | return this.player_.peertube().getCurrentResolutionLabel() | ||
84 | } | ||
85 | } | ||
86 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
87 | |||
88 | MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | ||
diff --git a/client/src/assets/player/resolution-menu-item.ts b/client/src/assets/player/resolution-menu-item.ts deleted file mode 100644 index b54fd91ef..000000000 --- a/client/src/assets/player/resolution-menu-item.ts +++ /dev/null | |||
@@ -1,67 +0,0 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
6 | |||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class ResolutionMenuItem extends MenuItem { | ||
9 | |||
10 | constructor (player: Player, options: any) { | ||
11 | const currentResolutionId = player.peertube().getCurrentResolutionId() | ||
12 | options.selectable = true | ||
13 | options.selected = options.id === currentResolutionId | ||
14 | |||
15 | super(player, options) | ||
16 | |||
17 | this.label = options.label | ||
18 | this.id = options.id | ||
19 | |||
20 | player.peertube().on('videoFileUpdate', () => this.updateSelection()) | ||
21 | player.peertube().on('autoResolutionUpdate', () => this.updateSelection()) | ||
22 | } | ||
23 | |||
24 | handleClick (event: any) { | ||
25 | if (this.id === -1 && this.player_.peertube().isAutoResolutionForbidden()) return | ||
26 | |||
27 | super.handleClick(event) | ||
28 | |||
29 | // Auto resolution | ||
30 | if (this.id === -1) { | ||
31 | this.player_.peertube().enableAutoResolution() | ||
32 | return | ||
33 | } | ||
34 | |||
35 | this.player_.peertube().disableAutoResolution() | ||
36 | this.player_.peertube().updateResolution(this.id) | ||
37 | } | ||
38 | |||
39 | updateSelection () { | ||
40 | // Check if auto resolution is forbidden or not | ||
41 | if (this.id === -1) { | ||
42 | if (this.player_.peertube().isAutoResolutionForbidden()) { | ||
43 | this.addClass('disabled') | ||
44 | } else { | ||
45 | this.removeClass('disabled') | ||
46 | } | ||
47 | } | ||
48 | |||
49 | if (this.player_.peertube().isAutoResolutionOn()) { | ||
50 | this.selected(this.id === -1) | ||
51 | return | ||
52 | } | ||
53 | |||
54 | this.selected(this.player_.peertube().getCurrentResolutionId() === this.id) | ||
55 | } | ||
56 | |||
57 | getLabel () { | ||
58 | if (this.id === -1) { | ||
59 | return this.label + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>' | ||
60 | } | ||
61 | |||
62 | return this.label | ||
63 | } | ||
64 | } | ||
65 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
66 | |||
67 | export { ResolutionMenuItem } | ||
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index 8b9f34b99..8d87567c2 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts | |||
@@ -112,9 +112,23 @@ function videoFileMinByResolution (files: VideoFile[]) { | |||
112 | return min | 112 | return min |
113 | } | 113 | } |
114 | 114 | ||
115 | function getRtcConfig () { | ||
116 | return { | ||
117 | iceServers: [ | ||
118 | { | ||
119 | urls: 'stun:stun.stunprotocol.org' | ||
120 | }, | ||
121 | { | ||
122 | urls: 'stun:stun.framasoft.org' | ||
123 | } | ||
124 | ] | ||
125 | } | ||
126 | } | ||
127 | |||
115 | // --------------------------------------------------------------------------- | 128 | // --------------------------------------------------------------------------- |
116 | 129 | ||
117 | export { | 130 | export { |
131 | getRtcConfig, | ||
118 | toTitleCase, | 132 | toTitleCase, |
119 | timeToInt, | 133 | timeToInt, |
120 | buildVideoLink, | 134 | buildVideoLink, |
diff --git a/client/src/assets/player/webtorrent-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts index c3c1af951..6424787b2 100644 --- a/client/src/assets/player/webtorrent-info-button.ts +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { bytes } from './utils' | 2 | import { bytes } from '../utils' |
3 | 3 | ||
4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
5 | class WebtorrentInfoButton extends Button { | 5 | class P2pInfoButton extends Button { |
6 | 6 | ||
7 | createEl () { | 7 | createEl () { |
8 | const div = videojsUntyped.dom.createEl('div', { | 8 | const div = videojsUntyped.dom.createEl('div', { |
@@ -65,7 +65,7 @@ class WebtorrentInfoButton extends Button { | |||
65 | subDivHttp.appendChild(subDivHttpText) | 65 | subDivHttp.appendChild(subDivHttpText) |
66 | div.appendChild(subDivHttp) | 66 | div.appendChild(subDivHttp) |
67 | 67 | ||
68 | this.player_.peertube().on('torrentInfo', (event: any, data: any) => { | 68 | this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => { |
69 | // We are in HTTP fallback | 69 | // We are in HTTP fallback |
70 | if (!data) { | 70 | if (!data) { |
71 | subDivHttp.className = 'vjs-peertube-displayed' | 71 | subDivHttp.className = 'vjs-peertube-displayed' |
@@ -74,11 +74,14 @@ class WebtorrentInfoButton extends Button { | |||
74 | return | 74 | return |
75 | } | 75 | } |
76 | 76 | ||
77 | const downloadSpeed = bytes(data.downloadSpeed) | 77 | const p2pStats = data.p2p |
78 | const uploadSpeed = bytes(data.uploadSpeed) | 78 | const httpStats = data.http |
79 | const totalDownloaded = bytes(data.downloaded) | 79 | |
80 | const totalUploaded = bytes(data.uploaded) | 80 | const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed) |
81 | const numPeers = data.numPeers | 81 | const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed) |
82 | const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded) | ||
83 | const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) | ||
84 | const numPeers = p2pStats.numPeers | ||
82 | 85 | ||
83 | subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + | 86 | subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + |
84 | this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) | 87 | this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) |
@@ -90,7 +93,7 @@ class WebtorrentInfoButton extends Button { | |||
90 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] | 93 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] |
91 | 94 | ||
92 | peersNumber.textContent = numPeers | 95 | peersNumber.textContent = numPeers |
93 | peersText.textContent = ' ' + this.player_.localize('peers') | 96 | peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer')) |
94 | 97 | ||
95 | subDivHttp.className = 'vjs-peertube-hidden' | 98 | subDivHttp.className = 'vjs-peertube-hidden' |
96 | subDivWebtorrent.className = 'vjs-peertube-displayed' | 99 | subDivWebtorrent.className = 'vjs-peertube-displayed' |
@@ -99,4 +102,4 @@ class WebtorrentInfoButton extends Button { | |||
99 | return div | 102 | return div |
100 | } | 103 | } |
101 | } | 104 | } |
102 | Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) | 105 | Button.registerComponent('P2PInfoButton', P2pInfoButton) |
diff --git a/client/src/assets/player/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts index de9a49de9..fed8ea33e 100644 --- a/client/src/assets/player/peertube-link-button.ts +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { buildVideoLink } from './utils' | 2 | import { buildVideoLink } from '../utils' |
3 | // FIXME: something weird with our path definition in tsconfig and typings | 3 | // FIXME: something weird with our path definition in tsconfig and typings |
4 | // @ts-ignore | 4 | // @ts-ignore |
5 | import { Player } from 'video.js' | 5 | import { Player } from 'video.js' |
diff --git a/client/src/assets/player/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts index af276d1b2..9a0e3b550 100644 --- a/client/src/assets/player/peertube-load-progress-bar.ts +++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | // FIXME: something weird with our path definition in tsconfig and typings | 2 | // FIXME: something weird with our path definition in tsconfig and typings |
3 | // @ts-ignore | 3 | // @ts-ignore |
4 | import { Player } from 'video.js' | 4 | import { Player } from 'video.js' |
@@ -27,7 +27,7 @@ class PeerTubeLoadProgressBar extends Component { | |||
27 | } | 27 | } |
28 | 28 | ||
29 | update () { | 29 | update () { |
30 | const torrent = this.player().peertube().getTorrent() | 30 | const torrent = this.player().webtorrent().getTorrent() |
31 | if (!torrent) return | 31 | if (!torrent) return |
32 | 32 | ||
33 | this.el_.style.width = (torrent.progress * 100) + '%' | 33 | this.el_.style.width = (torrent.progress * 100) + '%' |
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts new file mode 100644 index 000000000..abcc16411 --- /dev/null +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts | |||
@@ -0,0 +1,109 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
7 | |||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | ||
9 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
10 | class ResolutionMenuButton extends MenuButton { | ||
11 | label: HTMLElement | ||
12 | |||
13 | constructor (player: Player, options: any) { | ||
14 | super(player, options) | ||
15 | this.player = player | ||
16 | |||
17 | player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) | ||
18 | |||
19 | player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) | ||
20 | } | ||
21 | |||
22 | createEl () { | ||
23 | const el = super.createEl() | ||
24 | |||
25 | this.labelEl_ = videojsUntyped.dom.createEl('div', { | ||
26 | className: 'vjs-resolution-value' | ||
27 | }) | ||
28 | |||
29 | el.appendChild(this.labelEl_) | ||
30 | |||
31 | return el | ||
32 | } | ||
33 | |||
34 | updateARIAAttributes () { | ||
35 | this.el().setAttribute('aria-label', 'Quality') | ||
36 | } | ||
37 | |||
38 | createMenu () { | ||
39 | return new Menu(this.player_) | ||
40 | } | ||
41 | |||
42 | buildCSSClass () { | ||
43 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
44 | } | ||
45 | |||
46 | buildWrapperCSSClass () { | ||
47 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
48 | } | ||
49 | |||
50 | private addClickListener (component: any) { | ||
51 | component.on('click', () => { | ||
52 | let children = this.menu.children() | ||
53 | |||
54 | for (const child of children) { | ||
55 | if (component !== child) { | ||
56 | child.selected(false) | ||
57 | } | ||
58 | } | ||
59 | }) | ||
60 | } | ||
61 | |||
62 | private buildQualities (data: LoadedQualityData) { | ||
63 | // The automatic resolution item will need other labels | ||
64 | const labels: { [ id: number ]: string } = {} | ||
65 | |||
66 | data.qualityData.video.sort((a, b) => { | ||
67 | if (a.id > b.id) return -1 | ||
68 | if (a.id === b.id) return 0 | ||
69 | return 1 | ||
70 | }) | ||
71 | |||
72 | for (const d of data.qualityData.video) { | ||
73 | // Skip auto resolution, we'll add it ourselves | ||
74 | if (d.id === -1) continue | ||
75 | |||
76 | this.menu.addChild(new ResolutionMenuItem( | ||
77 | this.player_, | ||
78 | { | ||
79 | id: d.id, | ||
80 | label: d.label, | ||
81 | selected: d.selected, | ||
82 | callback: data.qualitySwitchCallback | ||
83 | }) | ||
84 | ) | ||
85 | |||
86 | labels[d.id] = d.label | ||
87 | } | ||
88 | |||
89 | this.menu.addChild(new ResolutionMenuItem( | ||
90 | this.player_, | ||
91 | { | ||
92 | id: -1, | ||
93 | label: this.player_.localize('Auto'), | ||
94 | labels, | ||
95 | callback: data.qualitySwitchCallback, | ||
96 | selected: true // By default, in auto mode | ||
97 | } | ||
98 | )) | ||
99 | |||
100 | for (const m of this.menu.children()) { | ||
101 | this.addClickListener(m) | ||
102 | } | ||
103 | |||
104 | this.trigger('menuChanged') | ||
105 | } | ||
106 | } | ||
107 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
108 | |||
109 | MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | ||
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts new file mode 100644 index 000000000..6c42fefd2 --- /dev/null +++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts | |||
@@ -0,0 +1,83 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | |||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class ResolutionMenuItem extends MenuItem { | ||
9 | private readonly id: number | ||
10 | private readonly label: string | ||
11 | // Only used for the automatic item | ||
12 | private readonly labels: { [id: number]: string } | ||
13 | private readonly callback: Function | ||
14 | |||
15 | private autoResolutionPossible: boolean | ||
16 | private currentResolutionLabel: string | ||
17 | |||
18 | constructor (player: Player, options: any) { | ||
19 | options.selectable = true | ||
20 | |||
21 | super(player, options) | ||
22 | |||
23 | this.autoResolutionPossible = true | ||
24 | this.currentResolutionLabel = '' | ||
25 | |||
26 | this.label = options.label | ||
27 | this.labels = options.labels | ||
28 | this.id = options.id | ||
29 | this.callback = options.callback | ||
30 | |||
31 | player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) | ||
32 | |||
33 | // We only want to disable the "Auto" item | ||
34 | if (this.id === -1) { | ||
35 | player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) | ||
36 | } | ||
37 | } | ||
38 | |||
39 | handleClick (event: any) { | ||
40 | // Auto button disabled? | ||
41 | if (this.autoResolutionPossible === false && this.id === -1) return | ||
42 | |||
43 | super.handleClick(event) | ||
44 | |||
45 | this.callback(this.id, 'video') | ||
46 | } | ||
47 | |||
48 | updateSelection (data: ResolutionUpdateData) { | ||
49 | if (this.id === -1) { | ||
50 | this.currentResolutionLabel = this.labels[data.id] | ||
51 | } | ||
52 | |||
53 | // Automatic resolution only | ||
54 | if (data.auto === true) { | ||
55 | this.selected(this.id === -1) | ||
56 | return | ||
57 | } | ||
58 | |||
59 | this.selected(this.id === data.id) | ||
60 | } | ||
61 | |||
62 | updateAutoResolution (data: AutoResolutionUpdateData) { | ||
63 | // Check if the auto resolution is enabled or not | ||
64 | if (data.possible === false) { | ||
65 | this.addClass('disabled') | ||
66 | } else { | ||
67 | this.removeClass('disabled') | ||
68 | } | ||
69 | |||
70 | this.autoResolutionPossible = data.possible | ||
71 | } | ||
72 | |||
73 | getLabel () { | ||
74 | if (this.id === -1) { | ||
75 | return this.label + ' <small>' + this.currentResolutionLabel + '</small>' | ||
76 | } | ||
77 | |||
78 | return this.label | ||
79 | } | ||
80 | } | ||
81 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
82 | |||
83 | export { ResolutionMenuItem } | ||
diff --git a/client/src/assets/player/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts index a7aefdcc3..14cb8ba43 100644 --- a/client/src/assets/player/settings-menu-button.ts +++ b/client/src/assets/player/videojs-components/settings-menu-button.ts | |||
@@ -6,8 +6,8 @@ | |||
6 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | 7 | ||
8 | import { SettingsMenuItem } from './settings-menu-item' | 8 | import { SettingsMenuItem } from './settings-menu-item' |
9 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
10 | import { toTitleCase } from './utils' | 10 | import { toTitleCase } from '../utils' |
11 | 11 | ||
12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') |
diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts index 2a3460ae5..f14959f9c 100644 --- a/client/src/assets/player/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts | |||
@@ -5,8 +5,8 @@ | |||
5 | // @ts-ignore | 5 | // @ts-ignore |
6 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | 7 | ||
8 | import { toTitleCase } from './utils' | 8 | import { toTitleCase } from '../utils' |
9 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
10 | 10 | ||
11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | 11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') |
12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') |
@@ -220,12 +220,14 @@ class SettingsMenuItem extends MenuItem { | |||
220 | } | 220 | } |
221 | 221 | ||
222 | build () { | 222 | build () { |
223 | const saveUpdateLabel = this.subMenu.updateLabel | 223 | this.subMenu.on('updateLabel', () => { |
224 | this.subMenu.updateLabel = () => { | ||
225 | this.update() | 224 | this.update() |
226 | 225 | }) | |
227 | saveUpdateLabel.call(this.subMenu) | 226 | this.subMenu.on('menuChanged', () => { |
228 | } | 227 | this.bindClickEvents() |
228 | this.setSize() | ||
229 | this.update() | ||
230 | }) | ||
229 | 231 | ||
230 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) | 232 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) |
231 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | 233 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) |
@@ -233,7 +235,7 @@ class SettingsMenuItem extends MenuItem { | |||
233 | this.update() | 235 | this.update() |
234 | 236 | ||
235 | this.createBackButton() | 237 | this.createBackButton() |
236 | this.getSize() | 238 | this.setSize() |
237 | this.bindClickEvents() | 239 | this.bindClickEvents() |
238 | 240 | ||
239 | // prefixed event listeners for CSS TransitionEnd | 241 | // prefixed event listeners for CSS TransitionEnd |
@@ -295,8 +297,9 @@ class SettingsMenuItem extends MenuItem { | |||
295 | 297 | ||
296 | // save size of submenus on first init | 298 | // save size of submenus on first init |
297 | // if number of submenu items change dynamically more logic will be needed | 299 | // if number of submenu items change dynamically more logic will be needed |
298 | getSize () { | 300 | setSize () { |
299 | this.dialog.removeClass('vjs-hidden') | 301 | this.dialog.removeClass('vjs-hidden') |
302 | videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
300 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) | 303 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) |
301 | this.setMargin() | 304 | this.setMargin() |
302 | this.dialog.addClass('vjs-hidden') | 305 | this.dialog.addClass('vjs-hidden') |
diff --git a/client/src/assets/player/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts index 4f8fede3d..1e11a9546 100644 --- a/client/src/assets/player/theater-button.ts +++ b/client/src/assets/player/videojs-components/theater-button.ts | |||
@@ -2,8 +2,8 @@ | |||
2 | // @ts-ignore | 2 | // @ts-ignore |
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 5 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
6 | import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' | 6 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' |
7 | 7 | ||
8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
9 | class TheaterButton extends Button { | 9 | class TheaterButton extends Button { |
diff --git a/client/src/assets/player/peertube-chunk-store.ts b/client/src/assets/player/webtorrent/peertube-chunk-store.ts index 54cc0ea64..54cc0ea64 100644 --- a/client/src/assets/player/peertube-chunk-store.ts +++ b/client/src/assets/player/webtorrent/peertube-chunk-store.ts | |||
diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/webtorrent/video-renderer.ts index a3415937b..a3415937b 100644 --- a/client/src/assets/player/video-renderer.ts +++ b/client/src/assets/player/webtorrent/video-renderer.ts | |||
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index e9fb90c61..c69bf31fa 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts | |||
@@ -3,23 +3,18 @@ | |||
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import * as WebTorrent from 'webtorrent' | 5 | import * as WebTorrent from 'webtorrent' |
6 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 6 | import { VideoFile } from '../../../../../shared/models/videos/video.model' |
7 | import { renderVideo } from './video-renderer' | 7 | import { renderVideo } from './video-renderer' |
8 | import './settings-menu-button' | 8 | import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
9 | import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' |
10 | import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils' | ||
11 | import { PeertubeChunkStore } from './peertube-chunk-store' | 10 | import { PeertubeChunkStore } from './peertube-chunk-store' |
12 | import { | 11 | import { |
13 | getAverageBandwidthInStore, | 12 | getAverageBandwidthInStore, |
14 | getStoredLastSubtitle, | ||
15 | getStoredMute, | 13 | getStoredMute, |
16 | getStoredVolume, | 14 | getStoredVolume, |
17 | getStoredWebTorrentEnabled, | 15 | getStoredWebTorrentEnabled, |
18 | saveAverageBandwidth, | 16 | saveAverageBandwidth |
19 | saveLastSubtitle, | 17 | } from '../peertube-player-local-storage' |
20 | saveMuteInStore, | ||
21 | saveVolumeInStore | ||
22 | } from './peertube-player-local-storage' | ||
23 | 18 | ||
24 | const CacheChunkStore = require('cache-chunk-store') | 19 | const CacheChunkStore = require('cache-chunk-store') |
25 | 20 | ||
@@ -30,14 +25,13 @@ type PlayOptions = { | |||
30 | } | 25 | } |
31 | 26 | ||
32 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 27 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') |
33 | class PeerTubePlugin extends Plugin { | 28 | class WebTorrentPlugin extends Plugin { |
34 | private readonly playerElement: HTMLVideoElement | 29 | private readonly playerElement: HTMLVideoElement |
35 | 30 | ||
36 | private readonly autoplay: boolean = false | 31 | private readonly autoplay: boolean = false |
37 | private readonly startTime: number = 0 | 32 | private readonly startTime: number = 0 |
38 | private readonly savePlayerSrcFunction: Function | 33 | private readonly savePlayerSrcFunction: Function |
39 | private readonly videoFiles: VideoFile[] | 34 | private readonly videoFiles: VideoFile[] |
40 | private readonly videoViewUrl: string | ||
41 | private readonly videoDuration: number | 35 | private readonly videoDuration: number |
42 | private readonly CONSTANTS = { | 36 | private readonly CONSTANTS = { |
43 | INFO_SCHEDULER: 1000, // Don't change this | 37 | INFO_SCHEDULER: 1000, // Don't change this |
@@ -45,22 +39,12 @@ class PeerTubePlugin extends Plugin { | |||
45 | AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it | 39 | AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it |
46 | AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check | 40 | AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check |
47 | AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds | 41 | AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds |
48 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5, // Last 5 seconds to build average bandwidth | 42 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth |
49 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | ||
50 | } | 43 | } |
51 | 44 | ||
52 | private readonly webtorrent = new WebTorrent({ | 45 | private readonly webtorrent = new WebTorrent({ |
53 | tracker: { | 46 | tracker: { |
54 | rtcConfig: { | 47 | rtcConfig: getRtcConfig() |
55 | iceServers: [ | ||
56 | { | ||
57 | urls: 'stun:stun.stunprotocol.org' | ||
58 | }, | ||
59 | { | ||
60 | urls: 'stun:stun.framasoft.org' | ||
61 | } | ||
62 | ] | ||
63 | } | ||
64 | }, | 48 | }, |
65 | dht: false | 49 | dht: false |
66 | }) | 50 | }) |
@@ -68,46 +52,37 @@ class PeerTubePlugin extends Plugin { | |||
68 | private player: any | 52 | private player: any |
69 | private currentVideoFile: VideoFile | 53 | private currentVideoFile: VideoFile |
70 | private torrent: WebTorrent.Torrent | 54 | private torrent: WebTorrent.Torrent |
71 | private videoCaptions: VideoJSCaption[] | ||
72 | private defaultSubtitle: string | ||
73 | 55 | ||
74 | private renderer: any | 56 | private renderer: any |
75 | private fakeRenderer: any | 57 | private fakeRenderer: any |
76 | private destroyingFakeRenderer = false | 58 | private destroyingFakeRenderer = false |
77 | 59 | ||
78 | private autoResolution = true | 60 | private autoResolution = true |
79 | private forbidAutoResolution = false | 61 | private autoResolutionPossible = true |
80 | private isAutoResolutionObservation = false | 62 | private isAutoResolutionObservation = false |
81 | private playerRefusedP2P = false | 63 | private playerRefusedP2P = false |
82 | 64 | ||
83 | private videoViewInterval: any | ||
84 | private torrentInfoInterval: any | 65 | private torrentInfoInterval: any |
85 | private autoQualityInterval: any | 66 | private autoQualityInterval: any |
86 | private userWatchingVideoInterval: any | ||
87 | private addTorrentDelay: any | 67 | private addTorrentDelay: any |
88 | private qualityObservationTimer: any | 68 | private qualityObservationTimer: any |
89 | private runAutoQualitySchedulerTimer: any | 69 | private runAutoQualitySchedulerTimer: any |
90 | 70 | ||
91 | private downloadSpeeds: number[] = [] | 71 | private downloadSpeeds: number[] = [] |
92 | 72 | ||
93 | constructor (player: videojs.Player, options: PeertubePluginOptions) { | 73 | constructor (player: videojs.Player, options: WebtorrentPluginOptions) { |
94 | super(player, options) | 74 | super(player, options) |
95 | 75 | ||
96 | // Disable auto play on iOS | 76 | // Disable auto play on iOS |
97 | this.autoplay = options.autoplay && this.isIOS() === false | 77 | this.autoplay = options.autoplay && this.isIOS() === false |
98 | this.playerRefusedP2P = !getStoredWebTorrentEnabled() | 78 | this.playerRefusedP2P = !getStoredWebTorrentEnabled() |
99 | 79 | ||
100 | this.startTime = timeToInt(options.startTime) | ||
101 | this.videoFiles = options.videoFiles | 80 | this.videoFiles = options.videoFiles |
102 | this.videoViewUrl = options.videoViewUrl | ||
103 | this.videoDuration = options.videoDuration | 81 | this.videoDuration = options.videoDuration |
104 | this.videoCaptions = options.videoCaptions | ||
105 | 82 | ||
106 | this.savePlayerSrcFunction = this.player.src | 83 | this.savePlayerSrcFunction = this.player.src |
107 | this.playerElement = options.playerElement | 84 | this.playerElement = options.playerElement |
108 | 85 | ||
109 | if (this.autoplay === true) this.player.addClass('vjs-has-autoplay') | ||
110 | |||
111 | this.player.ready(() => { | 86 | this.player.ready(() => { |
112 | const playerOptions = this.player.options_ | 87 | const playerOptions = this.player.options_ |
113 | 88 | ||
@@ -117,33 +92,10 @@ class PeerTubePlugin extends Plugin { | |||
117 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | 92 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() |
118 | if (muted !== undefined) this.player.muted(muted) | 93 | if (muted !== undefined) this.player.muted(muted) |
119 | 94 | ||
120 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | ||
121 | |||
122 | this.player.on('volumechange', () => { | ||
123 | saveVolumeInStore(this.player.volume()) | ||
124 | saveMuteInStore(this.player.muted()) | ||
125 | }) | ||
126 | |||
127 | this.player.textTracks().on('change', () => { | ||
128 | const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { | ||
129 | return t.kind === 'captions' && t.mode === 'showing' | ||
130 | }) | ||
131 | |||
132 | if (!showing) { | ||
133 | saveLastSubtitle('off') | ||
134 | return | ||
135 | } | ||
136 | |||
137 | saveLastSubtitle(showing.language) | ||
138 | }) | ||
139 | |||
140 | this.player.duration(options.videoDuration) | 95 | this.player.duration(options.videoDuration) |
141 | 96 | ||
142 | this.initializePlayer() | 97 | this.initializePlayer() |
143 | this.runTorrentInfoScheduler() | 98 | this.runTorrentInfoScheduler() |
144 | this.runViewAdd() | ||
145 | |||
146 | if (options.userWatching) this.runUserWatchVideo(options.userWatching) | ||
147 | 99 | ||
148 | this.player.one('play', () => { | 100 | this.player.one('play', () => { |
149 | // Don't run immediately scheduler, wait some seconds the TCP connections are made | 101 | // Don't run immediately scheduler, wait some seconds the TCP connections are made |
@@ -157,12 +109,9 @@ class PeerTubePlugin extends Plugin { | |||
157 | clearTimeout(this.qualityObservationTimer) | 109 | clearTimeout(this.qualityObservationTimer) |
158 | clearTimeout(this.runAutoQualitySchedulerTimer) | 110 | clearTimeout(this.runAutoQualitySchedulerTimer) |
159 | 111 | ||
160 | clearInterval(this.videoViewInterval) | ||
161 | clearInterval(this.torrentInfoInterval) | 112 | clearInterval(this.torrentInfoInterval) |
162 | clearInterval(this.autoQualityInterval) | 113 | clearInterval(this.autoQualityInterval) |
163 | 114 | ||
164 | if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) | ||
165 | |||
166 | // Don't need to destroy renderer, video player will be destroyed | 115 | // Don't need to destroy renderer, video player will be destroyed |
167 | this.flushVideoFile(this.currentVideoFile, false) | 116 | this.flushVideoFile(this.currentVideoFile, false) |
168 | 117 | ||
@@ -173,13 +122,6 @@ class PeerTubePlugin extends Plugin { | |||
173 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 | 122 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 |
174 | } | 123 | } |
175 | 124 | ||
176 | getCurrentResolutionLabel () { | ||
177 | if (!this.currentVideoFile) return '' | ||
178 | |||
179 | const fps = this.currentVideoFile.fps >= 50 ? this.currentVideoFile.fps : '' | ||
180 | return this.currentVideoFile.resolution.label + fps | ||
181 | } | ||
182 | |||
183 | updateVideoFile ( | 125 | updateVideoFile ( |
184 | videoFile?: VideoFile, | 126 | videoFile?: VideoFile, |
185 | options: { | 127 | options: { |
@@ -228,7 +170,8 @@ class PeerTubePlugin extends Plugin { | |||
228 | return done() | 170 | return done() |
229 | }) | 171 | }) |
230 | 172 | ||
231 | this.trigger('videoFileUpdate') | 173 | this.changeQuality() |
174 | this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id }) | ||
232 | } | 175 | } |
233 | 176 | ||
234 | updateResolution (resolutionId: number, delay = 0) { | 177 | updateResolution (resolutionId: number, delay = 0) { |
@@ -262,28 +205,17 @@ class PeerTubePlugin extends Plugin { | |||
262 | } | 205 | } |
263 | } | 206 | } |
264 | 207 | ||
265 | isAutoResolutionOn () { | ||
266 | return this.autoResolution | ||
267 | } | ||
268 | |||
269 | enableAutoResolution () { | 208 | enableAutoResolution () { |
270 | this.autoResolution = true | 209 | this.autoResolution = true |
271 | this.trigger('autoResolutionUpdate') | 210 | this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) |
272 | } | 211 | } |
273 | 212 | ||
274 | disableAutoResolution (forbid = false) { | 213 | disableAutoResolution (forbid = false) { |
275 | if (forbid === true) this.forbidAutoResolution = true | 214 | if (forbid === true) this.autoResolutionPossible = false |
276 | 215 | ||
277 | this.autoResolution = false | 216 | this.autoResolution = false |
278 | this.trigger('autoResolutionUpdate') | 217 | this.trigger('autoResolutionChange', { possible: this.autoResolutionPossible }) |
279 | } | 218 | this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) |
280 | |||
281 | isAutoResolutionForbidden () { | ||
282 | return this.forbidAutoResolution === true | ||
283 | } | ||
284 | |||
285 | getCurrentVideoFile () { | ||
286 | return this.currentVideoFile | ||
287 | } | 219 | } |
288 | 220 | ||
289 | getTorrent () { | 221 | getTorrent () { |
@@ -462,13 +394,7 @@ class PeerTubePlugin extends Plugin { | |||
462 | } | 394 | } |
463 | 395 | ||
464 | private initializePlayer () { | 396 | private initializePlayer () { |
465 | if (isMobile()) this.player.addClass('vjs-is-mobile') | 397 | this.buildQualities() |
466 | |||
467 | this.initSmoothProgressBar() | ||
468 | |||
469 | this.initCaptions() | ||
470 | |||
471 | this.alterInactivity() | ||
472 | 398 | ||
473 | if (this.autoplay === true) { | 399 | if (this.autoplay === true) { |
474 | this.player.posterImage.hide() | 400 | this.player.posterImage.hide() |
@@ -491,7 +417,7 @@ class PeerTubePlugin extends Plugin { | |||
491 | 417 | ||
492 | // Not initialized or in HTTP fallback | 418 | // Not initialized or in HTTP fallback |
493 | if (this.torrent === undefined || this.torrent === null) return | 419 | if (this.torrent === undefined || this.torrent === null) return |
494 | if (this.isAutoResolutionOn() === false) return | 420 | if (this.autoResolution === false) return |
495 | if (this.isAutoResolutionObservation === true) return | 421 | if (this.isAutoResolutionObservation === true) return |
496 | 422 | ||
497 | const file = this.getAppropriateFile() | 423 | const file = this.getAppropriateFile() |
@@ -531,78 +457,27 @@ class PeerTubePlugin extends Plugin { | |||
531 | if (this.torrent === undefined) return | 457 | if (this.torrent === undefined) return |
532 | 458 | ||
533 | // Http fallback | 459 | // Http fallback |
534 | if (this.torrent === null) return this.trigger('torrentInfo', false) | 460 | if (this.torrent === null) return this.player.trigger('p2pInfo', false) |
535 | 461 | ||
536 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too | 462 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too |
537 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) | 463 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) |
538 | 464 | ||
539 | return this.trigger('torrentInfo', { | 465 | return this.player.trigger('p2pInfo', { |
540 | downloadSpeed: this.torrent.downloadSpeed, | 466 | http: { |
541 | numPeers: this.torrent.numPeers, | 467 | downloadSpeed: 0, |
542 | uploadSpeed: this.torrent.uploadSpeed, | 468 | uploadSpeed: 0, |
543 | downloaded: this.torrent.downloaded, | 469 | downloaded: 0, |
544 | uploaded: this.torrent.uploaded | 470 | uploaded: 0 |
545 | }) | 471 | }, |
546 | }, this.CONSTANTS.INFO_SCHEDULER) | 472 | p2p: { |
547 | } | 473 | downloadSpeed: this.torrent.downloadSpeed, |
548 | 474 | numPeers: this.torrent.numPeers, | |
549 | private runViewAdd () { | 475 | uploadSpeed: this.torrent.uploadSpeed, |
550 | this.clearVideoViewInterval() | 476 | downloaded: this.torrent.downloaded, |
551 | 477 | uploaded: this.torrent.uploaded | |
552 | // After 30 seconds (or 3/4 of the video), add a view to the video | ||
553 | let minSecondsToView = 30 | ||
554 | |||
555 | if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 | ||
556 | |||
557 | let secondsViewed = 0 | ||
558 | this.videoViewInterval = setInterval(() => { | ||
559 | if (this.player && !this.player.paused()) { | ||
560 | secondsViewed += 1 | ||
561 | |||
562 | if (secondsViewed > minSecondsToView) { | ||
563 | this.clearVideoViewInterval() | ||
564 | |||
565 | this.addViewToVideo().catch(err => console.error(err)) | ||
566 | } | 478 | } |
567 | } | 479 | } as PlayerNetworkInfo) |
568 | }, 1000) | 480 | }, this.CONSTANTS.INFO_SCHEDULER) |
569 | } | ||
570 | |||
571 | private runUserWatchVideo (options: UserWatching) { | ||
572 | let lastCurrentTime = 0 | ||
573 | |||
574 | this.userWatchingVideoInterval = setInterval(() => { | ||
575 | const currentTime = Math.floor(this.player.currentTime()) | ||
576 | |||
577 | if (currentTime - lastCurrentTime >= 1) { | ||
578 | lastCurrentTime = currentTime | ||
579 | |||
580 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | ||
581 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
582 | } | ||
583 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | ||
584 | } | ||
585 | |||
586 | private clearVideoViewInterval () { | ||
587 | if (this.videoViewInterval !== undefined) { | ||
588 | clearInterval(this.videoViewInterval) | ||
589 | this.videoViewInterval = undefined | ||
590 | } | ||
591 | } | ||
592 | |||
593 | private addViewToVideo () { | ||
594 | if (!this.videoViewUrl) return Promise.resolve(undefined) | ||
595 | |||
596 | return fetch(this.videoViewUrl, { method: 'POST' }) | ||
597 | } | ||
598 | |||
599 | private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { | ||
600 | const body = new URLSearchParams() | ||
601 | body.append('currentTime', currentTime.toString()) | ||
602 | |||
603 | const headers = new Headers({ 'Authorization': authorizationHeader }) | ||
604 | |||
605 | return fetch(url, { method: 'PUT', body, headers }) | ||
606 | } | 481 | } |
607 | 482 | ||
608 | private fallbackToHttp (options: PlayOptions, done?: Function) { | 483 | private fallbackToHttp (options: PlayOptions, done?: Function) { |
@@ -620,8 +495,10 @@ class PeerTubePlugin extends Plugin { | |||
620 | this.player.src = this.savePlayerSrcFunction | 495 | this.player.src = this.savePlayerSrcFunction |
621 | this.player.src(httpUrl) | 496 | this.player.src(httpUrl) |
622 | 497 | ||
498 | this.changeQuality() | ||
499 | |||
623 | // We changed the source, so reinit captions | 500 | // We changed the source, so reinit captions |
624 | this.initCaptions() | 501 | this.player.trigger('sourcechange') |
625 | 502 | ||
626 | return this.tryToPlay(err => { | 503 | return this.tryToPlay(err => { |
627 | if (err && done) return done(err) | 504 | if (err && done) return done(err) |
@@ -649,25 +526,6 @@ class PeerTubePlugin extends Plugin { | |||
649 | return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) | 526 | return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) |
650 | } | 527 | } |
651 | 528 | ||
652 | private alterInactivity () { | ||
653 | let saveInactivityTimeout: number | ||
654 | |||
655 | const disableInactivity = () => { | ||
656 | saveInactivityTimeout = this.player.options_.inactivityTimeout | ||
657 | this.player.options_.inactivityTimeout = 0 | ||
658 | } | ||
659 | const enableInactivity = () => { | ||
660 | this.player.options_.inactivityTimeout = saveInactivityTimeout | ||
661 | } | ||
662 | |||
663 | const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog') | ||
664 | |||
665 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | ||
666 | settingsDialog.on('mouseenter', () => disableInactivity()) | ||
667 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | ||
668 | settingsDialog.on('mouseleave', () => enableInactivity()) | ||
669 | } | ||
670 | |||
671 | private pickAverageVideoFile () { | 529 | private pickAverageVideoFile () { |
672 | if (this.videoFiles.length === 1) return this.videoFiles[0] | 530 | if (this.videoFiles.length === 1) return this.videoFiles[0] |
673 | 531 | ||
@@ -712,43 +570,70 @@ class PeerTubePlugin extends Plugin { | |||
712 | } | 570 | } |
713 | } | 571 | } |
714 | 572 | ||
715 | private initCaptions () { | 573 | private buildQualities () { |
716 | for (const caption of this.videoCaptions) { | 574 | const qualityLevelsPayload = [] |
717 | this.player.addRemoteTextTrack({ | 575 | |
718 | kind: 'captions', | 576 | for (const file of this.videoFiles) { |
719 | label: caption.label, | 577 | const representation = { |
720 | language: caption.language, | 578 | id: file.resolution.id, |
721 | id: caption.language, | 579 | label: this.buildQualityLabel(file), |
722 | src: caption.src, | 580 | height: file.resolution.id, |
723 | default: this.defaultSubtitle === caption.language | 581 | _enabled: true |
724 | }, false) | 582 | } |
583 | |||
584 | this.player.qualityLevels().addQualityLevel(representation) | ||
585 | |||
586 | qualityLevelsPayload.push({ | ||
587 | id: representation.id, | ||
588 | label: representation.label, | ||
589 | selected: false | ||
590 | }) | ||
591 | } | ||
592 | |||
593 | const payload: LoadedQualityData = { | ||
594 | qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d), | ||
595 | qualityData: { | ||
596 | video: qualityLevelsPayload | ||
597 | } | ||
598 | } | ||
599 | this.player.tech_.trigger('loadedqualitydata', payload) | ||
600 | } | ||
601 | |||
602 | private buildQualityLabel (file: VideoFile) { | ||
603 | let label = file.resolution.label | ||
604 | |||
605 | if (file.fps && file.fps >= 50) { | ||
606 | label += file.fps | ||
725 | } | 607 | } |
726 | 608 | ||
727 | this.player.trigger('captionsChanged') | 609 | return label |
728 | } | 610 | } |
729 | 611 | ||
730 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | 612 | private qualitySwitchCallback (id: number) { |
731 | private initSmoothProgressBar () { | 613 | if (id === -1) { |
732 | const SeekBar = videojsUntyped.getComponent('SeekBar') | 614 | if (this.autoResolutionPossible === true) this.enableAutoResolution() |
733 | SeekBar.prototype.getPercent = function getPercent () { | 615 | return |
734 | // Allows for smooth scrubbing, when player can't keep up. | ||
735 | // const time = (this.player_.scrubbing()) ? | ||
736 | // this.player_.getCache().currentTime : | ||
737 | // this.player_.currentTime() | ||
738 | const time = this.player_.currentTime() | ||
739 | const percent = time / this.player_.duration() | ||
740 | return percent >= 1 ? 1 : percent | ||
741 | } | 616 | } |
742 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { | 617 | |
743 | let newTime = this.calculateDistance(event) * this.player_.duration() | 618 | this.disableAutoResolution() |
744 | if (newTime === this.player_.duration()) { | 619 | this.updateResolution(id) |
745 | newTime = newTime - 0.1 | 620 | } |
746 | } | 621 | |
747 | this.player_.currentTime(newTime) | 622 | private changeQuality () { |
748 | this.update() | 623 | const resolutionId = this.currentVideoFile.resolution.id |
624 | const qualityLevels = this.player.qualityLevels() | ||
625 | |||
626 | if (resolutionId === -1) { | ||
627 | qualityLevels.selectedIndex = -1 | ||
628 | return | ||
629 | } | ||
630 | |||
631 | for (let i = 0; i < qualityLevels; i++) { | ||
632 | const q = this.player.qualityLevels[i] | ||
633 | if (q.height === resolutionId) qualityLevels.selectedIndex = i | ||
749 | } | 634 | } |
750 | } | 635 | } |
751 | } | 636 | } |
752 | 637 | ||
753 | videojs.registerPlugin('peertube', PeerTubePlugin) | 638 | videojs.registerPlugin('webtorrent', WebTorrentPlugin) |
754 | export { PeerTubePlugin } | 639 | export { WebTorrentPlugin } |
diff --git a/client/src/index.html b/client/src/index.html index 2af0020ad..8c257824e 100644 --- a/client/src/index.html +++ b/client/src/index.html | |||
@@ -5,7 +5,7 @@ | |||
5 | <meta name="viewport" content="width=device-width, initial-scale=1"> | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
6 | 6 | ||
7 | <meta name="theme-color" content="#fff" /> | 7 | <meta name="theme-color" content="#fff" /> |
8 | 8 | <meta property="og:platform" content="PeerTube" /> | |
9 | <!-- Web Manifest file --> | 9 | <!-- Web Manifest file --> |
10 | <link rel="manifest" href="/manifest.webmanifest"> | 10 | <link rel="manifest" href="/manifest.webmanifest"> |
11 | 11 | ||
diff --git a/client/src/main.ts b/client/src/main.ts index dee962180..86fdabba5 100644 --- a/client/src/main.ts +++ b/client/src/main.ts | |||
@@ -34,7 +34,7 @@ const bootstrap = () => platformBrowserDynamic() | |||
34 | // .catch(err => console.error('Cannot register service worker.', err)) | 34 | // .catch(err => console.error('Cannot register service worker.', err)) |
35 | // } | 35 | // } |
36 | 36 | ||
37 | if (navigator.serviceWorker) { | 37 | if (navigator.serviceWorker && typeof navigator.serviceWorker.getRegistrations === 'function') { |
38 | navigator.serviceWorker.getRegistrations() | 38 | navigator.serviceWorker.getRegistrations() |
39 | .then(registrations => { | 39 | .then(registrations => { |
40 | for (const registration of registrations) { | 40 | for (const registration of registrations) { |
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index 2356f9837..478737a43 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss | |||
@@ -23,7 +23,7 @@ body { | |||
23 | // now beware node-sass requires interpolation | 23 | // now beware node-sass requires interpolation |
24 | // for css custom properties #{$var} | 24 | // for css custom properties #{$var} |
25 | --mainColor: #{$orange-color}; | 25 | --mainColor: #{$orange-color}; |
26 | --mainHoverColor: #{$orange-hoover-color}; | 26 | --mainHoverColor: #{$orange-hover-color}; |
27 | --mainBackgroundColor: #{$bg-color}; | 27 | --mainBackgroundColor: #{$bg-color}; |
28 | --mainForegroundColor: #{$fg-color}; | 28 | --mainForegroundColor: #{$fg-color}; |
29 | --menuBackgroundColor: #{$menu-background}; | 29 | --menuBackgroundColor: #{$menu-background}; |
@@ -229,13 +229,12 @@ label { | |||
229 | font-weight: $font-semibold; | 229 | font-weight: $font-semibold; |
230 | } | 230 | } |
231 | 231 | ||
232 | .close { | 232 | my-global-icon { |
233 | @include icon(24px); | 233 | @include icon(24px); |
234 | 234 | ||
235 | position: relative; | 235 | position: relative; |
236 | top: 3px; | 236 | top: 3px; |
237 | float: right; | 237 | float: right; |
238 | background-image: url('../assets/images/global/cross.svg'); | ||
239 | 238 | ||
240 | margin: 0; | 239 | margin: 0; |
241 | padding: 0; | 240 | padding: 0; |
@@ -293,6 +292,10 @@ ngb-tabset.bootstrap { | |||
293 | color: var(--mainForegroundColor) !important; | 292 | color: var(--mainForegroundColor) !important; |
294 | } | 293 | } |
295 | } | 294 | } |
295 | |||
296 | .nav-pills .nav-link.active { | ||
297 | color: #000 !important; | ||
298 | } | ||
296 | } | 299 | } |
297 | 300 | ||
298 | .nav-tabs .nav-link.active { | 301 | .nav-tabs .nav-link.active { |
@@ -324,7 +327,7 @@ ngb-tabset.bootstrap { | |||
324 | table { | 327 | table { |
325 | .action-button-edit, .action-button-delete { | 328 | .action-button-edit, .action-button-delete { |
326 | &:hover, &:active, &:focus, &[disabled], &.disabled { | 329 | &:hover, &:active, &:focus, &[disabled], &.disabled { |
327 | background-color: $grey-color !important; | 330 | background-color: $grey-background-color !important; |
328 | } | 331 | } |
329 | } | 332 | } |
330 | } | 333 | } |
@@ -389,4 +392,4 @@ table { | |||
389 | } | 392 | } |
390 | } | 393 | } |
391 | } | 394 | } |
392 | } \ No newline at end of file | 395 | } |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index d6f391a45..e18e9ae9d 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -55,6 +55,18 @@ | |||
55 | hyphens: auto; | 55 | hyphens: auto; |
56 | } | 56 | } |
57 | 57 | ||
58 | @mixin apply-svg-color ($color) { | ||
59 | /deep/ svg { | ||
60 | path[fill="#000000"], g[fill="#000000"], rect[fill="#000000"], circle[fill="#000000"] { | ||
61 | fill: $color; | ||
62 | } | ||
63 | |||
64 | path[stroke="#000000"], g[stroke="#000000"], rect[stroke="#000000"], circle[stroke="#000000"] { | ||
65 | stroke: $color; | ||
66 | } | ||
67 | } | ||
68 | } | ||
69 | |||
58 | @mixin peertube-input-text($width) { | 70 | @mixin peertube-input-text($width) { |
59 | display: inline-block; | 71 | display: inline-block; |
60 | height: $button-height; | 72 | height: $button-height; |
@@ -64,6 +76,7 @@ | |||
64 | border-radius: 3px; | 76 | border-radius: 3px; |
65 | padding-left: 15px; | 77 | padding-left: 15px; |
66 | padding-right: 15px; | 78 | padding-right: 15px; |
79 | font-size: 15px; | ||
67 | 80 | ||
68 | &::placeholder { | 81 | &::placeholder { |
69 | color: var(--inputPlaceholderColor); | 82 | color: var(--inputPlaceholderColor); |
@@ -110,22 +123,30 @@ | |||
110 | color: #fff; | 123 | color: #fff; |
111 | background-color: #C6C6C6; | 124 | background-color: #C6C6C6; |
112 | } | 125 | } |
126 | |||
127 | my-global-icon { | ||
128 | @include apply-svg-color(#fff) | ||
129 | } | ||
113 | } | 130 | } |
114 | 131 | ||
115 | @mixin grey-button { | 132 | @mixin grey-button { |
116 | &, &:active, &:focus { | 133 | &, &:active, &:focus { |
117 | background-color: $grey-color; | 134 | background-color: $grey-background-color; |
118 | color: #585858; | 135 | color: $grey-foreground-color; |
119 | } | 136 | } |
120 | 137 | ||
121 | &:hover, &:active, &:focus, &[disabled], &.disabled { | 138 | &:hover, &:active, &:focus, &[disabled], &.disabled { |
122 | color: #585858; | 139 | color: $grey-foreground-color; |
123 | background-color: $grey-hoover-color; | 140 | background-color: $grey-background-hover-color; |
124 | } | 141 | } |
125 | 142 | ||
126 | &[disabled], &.disabled { | 143 | &[disabled], &.disabled { |
127 | cursor: default; | 144 | cursor: default; |
128 | } | 145 | } |
146 | |||
147 | my-global-icon { | ||
148 | @include apply-svg-color($grey-foreground-color) | ||
149 | } | ||
129 | } | 150 | } |
130 | 151 | ||
131 | @mixin peertube-button { | 152 | @mixin peertube-button { |
@@ -148,6 +169,15 @@ | |||
148 | @include peertube-button; | 169 | @include peertube-button; |
149 | } | 170 | } |
150 | 171 | ||
172 | @mixin button-with-icon($width: 20px, $margin-right: 3px, $top: -1px) { | ||
173 | my-global-icon { | ||
174 | position: relative; | ||
175 | width: $width; | ||
176 | margin-right: $margin-right; | ||
177 | top: $top; | ||
178 | } | ||
179 | } | ||
180 | |||
151 | @mixin peertube-button-file ($width) { | 181 | @mixin peertube-button-file ($width) { |
152 | position: relative; | 182 | position: relative; |
153 | overflow: hidden; | 183 | overflow: hidden; |
@@ -231,6 +261,10 @@ | |||
231 | color: transparent; | 261 | color: transparent; |
232 | text-shadow: 0 0 0 #000; | 262 | text-shadow: 0 0 0 #000; |
233 | } | 263 | } |
264 | |||
265 | option { | ||
266 | color: #000; | ||
267 | } | ||
234 | } | 268 | } |
235 | } | 269 | } |
236 | 270 | ||
@@ -455,18 +489,10 @@ | |||
455 | } | 489 | } |
456 | } | 490 | } |
457 | 491 | ||
458 | @mixin create-button ($imageUrl) { | 492 | @mixin create-button { |
459 | @include peertube-button-link; | 493 | @include peertube-button-link; |
460 | @include orange-button; | 494 | @include orange-button; |
461 | 495 | @include button-with-icon(20px, 5px, -1px); | |
462 | .icon.icon-add { | ||
463 | @include icon(20px); | ||
464 | |||
465 | position: relative; | ||
466 | top: -1px; | ||
467 | margin-right: 5px; | ||
468 | background-image: url($imageUrl); | ||
469 | } | ||
470 | } | 496 | } |
471 | 497 | ||
472 | @mixin row-blocks { | 498 | @mixin row-blocks { |
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index fdf33b12a..3780b7501 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss | |||
@@ -6,10 +6,13 @@ $font-regular: 400; | |||
6 | $font-semibold: 600; | 6 | $font-semibold: 600; |
7 | $font-bold: 700; | 7 | $font-bold: 700; |
8 | 8 | ||
9 | $grey-color: #E5E5E5; | 9 | $grey-background-color: #E5E5E5; |
10 | $grey-hoover-color: #EFEFEF;; | 10 | $grey-background-hover-color: #EFEFEF; |
11 | $grey-foreground-color: #585858; | ||
12 | $grey-foreground-hover-color: #303030; | ||
13 | |||
11 | $orange-color: #F1680D; | 14 | $orange-color: #F1680D; |
12 | $orange-hoover-color: #F97D46; | 15 | $orange-hover-color: #F97D46; |
13 | 16 | ||
14 | $bg-color: #fff; | 17 | $bg-color: #fff; |
15 | $fg-color: #000; | 18 | $fg-color: #000; |
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index 6057b1db0..6e502b028 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -14,7 +14,7 @@ | |||
14 | p-table { | 14 | p-table { |
15 | .ui-table-caption { | 15 | .ui-table-caption { |
16 | border: none !important; | 16 | border: none !important; |
17 | background-color: #fff !important; | 17 | background-color: var(--mainBackgroundColor) !important; |
18 | 18 | ||
19 | .caption { | 19 | .caption { |
20 | height: 40px; | 20 | height: 40px; |
@@ -24,7 +24,7 @@ p-table { | |||
24 | } | 24 | } |
25 | 25 | ||
26 | th { | 26 | th { |
27 | background-color: #fff !important; | 27 | background-color: var(--mainBackgroundColor) !important; |
28 | outline: 0; | 28 | outline: 0; |
29 | } | 29 | } |
30 | 30 | ||
@@ -122,10 +122,14 @@ p-table { | |||
122 | 122 | ||
123 | &.pi-sort-up { | 123 | &.pi-sort-up { |
124 | @extend .glyphicon-triangle-top; | 124 | @extend .glyphicon-triangle-top; |
125 | |||
126 | color: var(--mainForegroundColor) !important; | ||
125 | } | 127 | } |
126 | 128 | ||
127 | &.pi-sort-down { | 129 | &.pi-sort-down { |
128 | @extend .glyphicon-triangle-bottom; | 130 | @extend .glyphicon-triangle-bottom; |
131 | |||
132 | color: var(--mainForegroundColor) !important; | ||
129 | } | 133 | } |
130 | } | 134 | } |
131 | } | 135 | } |
@@ -193,7 +197,7 @@ p-table { | |||
193 | height: auto !important; | 197 | height: auto !important; |
194 | 198 | ||
195 | a { | 199 | a { |
196 | color: #000 !important; | 200 | color: var(--mainForegroundColor) !important; |
197 | font-weight: $font-semibold !important; | 201 | font-weight: $font-semibold !important; |
198 | margin: 0 5px !important; | 202 | margin: 0 5px !important; |
199 | outline: 0 !important; | 203 | outline: 0 !important; |
@@ -230,6 +234,7 @@ p-calendar .ui-datepicker { | |||
230 | @extend .glyphicon-chevron-right; | 234 | @extend .glyphicon-chevron-right; |
231 | @include glyphicon-light; | 235 | @include glyphicon-light; |
232 | 236 | ||
237 | color: #000 !important; | ||
233 | text-align: right; | 238 | text-align: right; |
234 | 239 | ||
235 | .pi.pi-chevron-right { | 240 | .pi.pi-chevron-right { |
@@ -241,6 +246,7 @@ p-calendar .ui-datepicker { | |||
241 | @extend .glyphicon-chevron-left; | 246 | @extend .glyphicon-chevron-left; |
242 | @include glyphicon-light; | 247 | @include glyphicon-light; |
243 | 248 | ||
249 | color: #000 !important; | ||
244 | text-align: left; | 250 | text-align: left; |
245 | 251 | ||
246 | .pi.pi-chevron-left { | 252 | .pi.pi-chevron-left { |
@@ -254,42 +260,53 @@ p-calendar .ui-datepicker { | |||
254 | .pi.pi-chevron-up { | 260 | .pi.pi-chevron-up { |
255 | @extend .glyphicon-chevron-up; | 261 | @extend .glyphicon-chevron-up; |
256 | @include glyphicon-light; | 262 | @include glyphicon-light; |
263 | |||
264 | color: #000 !important; | ||
257 | } | 265 | } |
258 | 266 | ||
259 | .pi.pi-chevron-down { | 267 | .pi.pi-chevron-down { |
260 | @extend .glyphicon-chevron-down; | 268 | @extend .glyphicon-chevron-down; |
261 | @include glyphicon-light; | 269 | @include glyphicon-light; |
270 | |||
271 | color: #000 !important; | ||
262 | } | 272 | } |
263 | } | 273 | } |
264 | } | 274 | } |
265 | 275 | ||
276 | .ui-chkbox { | ||
266 | 277 | ||
267 | .ui-chkbox-box { | 278 | &, .ui-chkbox-box { |
268 | &.ui-state-active { | 279 | width: 18px !important; |
269 | border-color: var(--mainColor) !important; | 280 | height: 18px !important; |
270 | background-color: var(--mainColor) !important; | ||
271 | } | 281 | } |
272 | 282 | ||
273 | .ui-chkbox-icon { | 283 | .ui-chkbox-box { |
274 | position: relative; | 284 | &.ui-state-active { |
275 | overflow: visible !important; | 285 | border-color: var(--mainColor) !important; |
276 | 286 | background-color: var(--mainColor) !important; | |
277 | &:after { | ||
278 | content: ''; | ||
279 | position: absolute; | ||
280 | top: 1px; | ||
281 | left: 7px; | ||
282 | width: 5px; | ||
283 | height: 13px; | ||
284 | opacity: 0; | ||
285 | transform: rotate(45deg) scale(0); | ||
286 | border-right: 2px solid var(--mainBackgroundColor); | ||
287 | border-bottom: 2px solid var(--mainBackgroundColor); | ||
288 | } | 287 | } |
289 | 288 | ||
290 | &.pi-check:after { | 289 | .ui-chkbox-icon { |
291 | opacity: 1; | 290 | position: relative; |
292 | transform: rotate(45deg) scale(1); | 291 | overflow: visible !important; |
292 | |||
293 | &:after { | ||
294 | content: ''; | ||
295 | position: absolute; | ||
296 | top: 1px; | ||
297 | left: 6px; | ||
298 | width: 5px; | ||
299 | height: 12px; | ||
300 | opacity: 0; | ||
301 | transform: rotate(45deg) scale(0); | ||
302 | border-right: 2px solid var(--mainBackgroundColor); | ||
303 | border-bottom: 2px solid var(--mainBackgroundColor); | ||
304 | } | ||
305 | |||
306 | &.pi-check:after { | ||
307 | opacity: 1; | ||
308 | transform: rotate(45deg) scale(1); | ||
309 | } | ||
293 | } | 310 | } |
294 | } | 311 | } |
295 | } | 312 | } |
@@ -354,3 +371,7 @@ p-toast { | |||
354 | } | 371 | } |
355 | } | 372 | } |
356 | } | 373 | } |
374 | |||
375 | .ui-widget { | ||
376 | font-family: $main-fonts !important; | ||
377 | } | ||
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index f79cf68df..c3b6e08ca 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html | |||
@@ -6,6 +6,7 @@ | |||
6 | <meta charset="UTF-8"> | 6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width, initial-scale=1"> | 7 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
8 | <meta name="robots" content="noindex"> | 8 | <meta name="robots" content="noindex"> |
9 | <meta property="og:platform" content="PeerTube" /> | ||
9 | 10 | ||
10 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> | 11 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> |
11 | </head> | 12 | </head> |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 54b8fb543..32bf42e12 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -17,17 +17,19 @@ import 'core-js/es6/set' | |||
17 | // For google bot that uses Chrome 41 and does not understand fetch | 17 | // For google bot that uses Chrome 41 and does not understand fetch |
18 | import 'whatwg-fetch' | 18 | import 'whatwg-fetch' |
19 | 19 | ||
20 | // FIXME: something weird with our path definition in tsconfig and typings | ||
21 | // @ts-ignore | ||
22 | import * as vjs from 'video.js' | ||
23 | |||
24 | import * as Channel from 'jschannel' | 20 | import * as Channel from 'jschannel' |
25 | 21 | ||
26 | import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared' | 22 | import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared' |
27 | import { addContextMenu, getServerTranslations, getVideojsOptions, loadLocaleInVideoJS } from '../../assets/player/peertube-player' | ||
28 | import { PeerTubeResolution } from '../player/definitions' | 23 | import { PeerTubeResolution } from '../player/definitions' |
29 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' | 24 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' |
30 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' | 25 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' |
26 | import { | ||
27 | P2PMediaLoaderOptions, | ||
28 | PeertubePlayerManager, | ||
29 | PeertubePlayerManagerOptions, | ||
30 | PlayerMode | ||
31 | } from '../../assets/player/peertube-player-manager' | ||
32 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | ||
31 | 33 | ||
32 | /** | 34 | /** |
33 | * Embed API exposes control of the embed player to the outside world via | 35 | * Embed API exposes control of the embed player to the outside world via |
@@ -73,16 +75,16 @@ class PeerTubeEmbedApi { | |||
73 | } | 75 | } |
74 | 76 | ||
75 | private setResolution (resolutionId: number) { | 77 | private setResolution (resolutionId: number) { |
76 | if (resolutionId === -1 && this.embed.player.peertube().isAutoResolutionForbidden()) return | 78 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return |
77 | 79 | ||
78 | // Auto resolution | 80 | // Auto resolution |
79 | if (resolutionId === -1) { | 81 | if (resolutionId === -1) { |
80 | this.embed.player.peertube().enableAutoResolution() | 82 | this.embed.player.webtorrent().enableAutoResolution() |
81 | return | 83 | return |
82 | } | 84 | } |
83 | 85 | ||
84 | this.embed.player.peertube().disableAutoResolution() | 86 | this.embed.player.webtorrent().disableAutoResolution() |
85 | this.embed.player.peertube().updateResolution(resolutionId) | 87 | this.embed.player.webtorrent().updateResolution(resolutionId) |
86 | } | 88 | } |
87 | 89 | ||
88 | /** | 90 | /** |
@@ -122,15 +124,17 @@ class PeerTubeEmbedApi { | |||
122 | 124 | ||
123 | // PeerTube specific capabilities | 125 | // PeerTube specific capabilities |
124 | 126 | ||
125 | this.embed.player.peertube().on('autoResolutionUpdate', () => this.loadResolutions()) | 127 | if (this.embed.player.webtorrent) { |
126 | this.embed.player.peertube().on('videoFileUpdate', () => this.loadResolutions()) | 128 | this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) |
129 | this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) | ||
130 | } | ||
127 | } | 131 | } |
128 | 132 | ||
129 | private loadResolutions () { | 133 | private loadWebTorrentResolutions () { |
130 | let resolutions = [] | 134 | let resolutions = [] |
131 | let currentResolutionId = this.embed.player.peertube().getCurrentResolutionId() | 135 | let currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() |
132 | 136 | ||
133 | for (const videoFile of this.embed.player.peertube().videoFiles) { | 137 | for (const videoFile of this.embed.player.webtorrent().videoFiles) { |
134 | let label = videoFile.resolution.label | 138 | let label = videoFile.resolution.label |
135 | if (videoFile.fps && videoFile.fps >= 50) { | 139 | if (videoFile.fps && videoFile.fps >= 50) { |
136 | label += videoFile.fps | 140 | label += videoFile.fps |
@@ -164,6 +168,7 @@ class PeerTubeEmbed { | |||
164 | subtitle: string | 168 | subtitle: string |
165 | enableApi = false | 169 | enableApi = false |
166 | startTime: number | string = 0 | 170 | startTime: number | string = 0 |
171 | mode: PlayerMode | ||
167 | scope = 'peertube' | 172 | scope = 'peertube' |
168 | 173 | ||
169 | static async main () { | 174 | static async main () { |
@@ -257,6 +262,8 @@ class PeerTubeEmbed { | |||
257 | this.scope = this.getParamString(params, 'scope', this.scope) | 262 | this.scope = this.getParamString(params, 'scope', this.scope) |
258 | this.subtitle = this.getParamString(params, 'subtitle') | 263 | this.subtitle = this.getParamString(params, 'subtitle') |
259 | this.startTime = this.getParamString(params, 'start') | 264 | this.startTime = this.getParamString(params, 'start') |
265 | |||
266 | this.mode = this.getParamString(params, 'mode') === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' | ||
260 | } catch (err) { | 267 | } catch (err) { |
261 | console.error('Cannot get params from URL.', err) | 268 | console.error('Cannot get params from URL.', err) |
262 | } | 269 | } |
@@ -266,9 +273,8 @@ class PeerTubeEmbed { | |||
266 | const urlParts = window.location.pathname.split('/') | 273 | const urlParts = window.location.pathname.split('/') |
267 | const videoId = urlParts[ urlParts.length - 1 ] | 274 | const videoId = urlParts[ urlParts.length - 1 ] |
268 | 275 | ||
269 | const [ , serverTranslations, videoResponse, captionsResponse ] = await Promise.all([ | 276 | const [ serverTranslations, videoResponse, captionsResponse ] = await Promise.all([ |
270 | loadLocaleInVideoJS(window.location.origin, vjs, navigator.language), | 277 | PeertubePlayerManager.getServerTranslations(window.location.origin, navigator.language), |
271 | getServerTranslations(window.location.origin, navigator.language), | ||
272 | this.loadVideoInfo(videoId), | 278 | this.loadVideoInfo(videoId), |
273 | this.loadVideoCaptions(videoId) | 279 | this.loadVideoCaptions(videoId) |
274 | ]) | 280 | ]) |
@@ -292,43 +298,67 @@ class PeerTubeEmbed { | |||
292 | 298 | ||
293 | this.loadParams() | 299 | this.loadParams() |
294 | 300 | ||
295 | const videojsOptions = getVideojsOptions({ | 301 | const options: PeertubePlayerManagerOptions = { |
296 | autoplay: this.autoplay, | 302 | common: { |
297 | controls: this.controls, | 303 | autoplay: this.autoplay, |
298 | muted: this.muted, | 304 | controls: this.controls, |
299 | loop: this.loop, | 305 | muted: this.muted, |
300 | startTime: this.startTime, | 306 | loop: this.loop, |
301 | subtitle: this.subtitle, | 307 | captions: videoCaptions.length !== 0, |
302 | 308 | startTime: this.startTime, | |
303 | videoCaptions, | 309 | subtitle: this.subtitle, |
304 | inactivityTimeout: 1500, | 310 | |
305 | videoViewUrl: this.getVideoUrl(videoId) + '/views', | 311 | videoCaptions, |
306 | playerElement: this.videoElement, | 312 | inactivityTimeout: 1500, |
307 | videoFiles: videoInfo.files, | 313 | videoViewUrl: this.getVideoUrl(videoId) + '/views', |
308 | videoDuration: videoInfo.duration, | 314 | |
309 | enableHotkeys: true, | 315 | playerElement: this.videoElement, |
310 | peertubeLink: true, | 316 | onPlayerElementChange: (element: HTMLVideoElement) => this.videoElement = element, |
311 | poster: window.location.origin + videoInfo.previewPath, | 317 | |
312 | theaterMode: false | 318 | videoDuration: videoInfo.duration, |
313 | }) | 319 | enableHotkeys: true, |
320 | peertubeLink: true, | ||
321 | poster: window.location.origin + videoInfo.previewPath, | ||
322 | theaterMode: false, | ||
323 | |||
324 | serverUrl: window.location.origin, | ||
325 | language: navigator.language, | ||
326 | embedUrl: window.location.origin + videoInfo.embedPath | ||
327 | }, | ||
328 | |||
329 | webtorrent: { | ||
330 | videoFiles: videoInfo.files | ||
331 | } | ||
332 | } | ||
314 | 333 | ||
315 | this.playerOptions = videojsOptions | 334 | if (this.mode === 'p2p-media-loader') { |
316 | this.player = vjs(this.videoContainerId, videojsOptions, () => { | 335 | const hlsPlaylist = videoInfo.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) |
317 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) | 336 | |
337 | Object.assign(options, { | ||
338 | p2pMediaLoader: { | ||
339 | playlistUrl: hlsPlaylist.playlistUrl, | ||
340 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
341 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
342 | trackerAnnounce: videoInfo.trackerUrls, | ||
343 | videoFiles: videoInfo.files | ||
344 | } as P2PMediaLoaderOptions | ||
345 | }) | ||
346 | } | ||
318 | 347 | ||
319 | window[ 'videojsPlayer' ] = this.player | 348 | this.player = await PeertubePlayerManager.initialize(this.mode, options) |
320 | 349 | ||
321 | if (this.controls) { | 350 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) |
322 | this.player.dock({ | ||
323 | title: videoInfo.name, | ||
324 | description: this.player.localize('Uses P2P, others may know your IP is downloading this video.') | ||
325 | }) | ||
326 | } | ||
327 | 351 | ||
328 | addContextMenu(this.player, window.location.origin + videoInfo.embedPath) | 352 | window[ 'videojsPlayer' ] = this.player |
329 | 353 | ||
330 | this.initializeApi() | 354 | if (this.controls) { |
331 | }) | 355 | this.player.dock({ |
356 | title: videoInfo.name, | ||
357 | description: this.player.localize('Uses P2P, others may know your IP is downloading this video.') | ||
358 | }) | ||
359 | } | ||
360 | |||
361 | this.initializeApi() | ||
332 | } | 362 | } |
333 | 363 | ||
334 | private handleError (err: Error, translations?: { [ id: string ]: string }) { | 364 | private handleError (err: Error, translations?: { [ id: string ]: string }) { |
diff --git a/client/src/tsconfig.app.json b/client/src/tsconfig.app.json index af7a74e9e..729eee353 100644 --- a/client/src/tsconfig.app.json +++ b/client/src/tsconfig.app.json | |||
@@ -3,7 +3,7 @@ | |||
3 | "compilerOptions": { | 3 | "compilerOptions": { |
4 | "outDir": "../out-tsc/app", | 4 | "outDir": "../out-tsc/app", |
5 | "baseUrl": "./", | 5 | "baseUrl": "./", |
6 | "module": "es2015", | 6 | "module": "esnext", |
7 | "types": [], | 7 | "types": [], |
8 | "lib": [ | 8 | "lib": [ |
9 | "es2017", | 9 | "es2017", |