aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+videos/+video-watch/shared/metadata
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+videos/+video-watch/shared/metadata')
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/index.ts3
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html11
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss42
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts26
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html19
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss46
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts87
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.html23
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.scss15
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.ts142
10 files changed, 414 insertions, 0 deletions
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/index.ts b/client/src/app/+videos/+video-watch/shared/metadata/index.ts
new file mode 100644
index 000000000..ba97f7011
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/index.ts
@@ -0,0 +1,3 @@
1export * from './video-avatar-channel.component'
2export * from './video-description.component'
3export * from './video-rate.component'
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html
new file mode 100644
index 000000000..5a7221858
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html
@@ -0,0 +1,11 @@
1<div class="wrapper" [ngClass]="{ 'generic-channel': genericChannel }">
2 <my-actor-avatar
3 class="channel" [channel]="video.channel"
4 [internalHref]="[ '/c', video.byVideoChannel ]" [title]="channelLinkTitle"
5 ></my-actor-avatar>
6
7 <my-actor-avatar
8 class="account" [account]="video.account"
9 [internalHref]="[ '/a', video.byAccount ]" [title]="accountLinkTitle">
10 </my-actor-avatar>
11</div>
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss
new file mode 100644
index 000000000..1ff8fb96e
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss
@@ -0,0 +1,42 @@
1@use '_mixins' as *;
2
3@mixin main {
4 @include actor-avatar-size(35px);
5}
6
7@mixin secondary {
8 height: 60%;
9 width: 60%;
10 position: absolute;
11 bottom: -5px;
12 right: -5px;
13 background-color: rgba(0, 0, 0, 0);
14}
15
16.wrapper {
17 @include actor-avatar-size(35px);
18 @include margin-right(5px);
19
20 position: relative;
21 margin-bottom: 5px;
22
23 &.generic-channel {
24 .account {
25 @include main();
26 }
27
28 .channel {
29 display: none !important;
30 }
31 }
32
33 &:not(.generic-channel) {
34 .account {
35 @include secondary();
36 }
37
38 .channel {
39 @include main();
40 }
41 }
42}
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts
new file mode 100644
index 000000000..63edd7bad
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts
@@ -0,0 +1,26 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { Video } from '@app/shared/shared-main/video'
3
4@Component({
5 selector: 'my-video-avatar-channel',
6 templateUrl: './video-avatar-channel.component.html',
7 styleUrls: [ './video-avatar-channel.component.scss' ]
8})
9export class VideoAvatarChannelComponent implements OnInit {
10 @Input() video: Video
11 @Input() byAccount: string
12
13 @Input() genericChannel: boolean
14
15 channelLinkTitle = ''
16 accountLinkTitle = ''
17
18 ngOnInit () {
19 this.channelLinkTitle = $localize`${this.video.account.name} (channel page)`
20 this.accountLinkTitle = $localize`${this.video.byAccount} (account page)`
21 }
22
23 isChannelAvatarNull () {
24 return this.video.channel.avatar === null
25 }
26}
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html
new file mode 100644
index 000000000..57f682899
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html
@@ -0,0 +1,19 @@
1<div class="video-info-description">
2 <div
3 class="video-info-description-html"
4 [innerHTML]="videoHTMLDescription"
5 (timestampClicked)="onTimestampClicked($event)"
6 timestampRouteTransformer
7 ></div>
8
9 <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length >= 250" (click)="showMoreDescription()">
10 <ng-container i18n>Show more</ng-container>
11 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span>
12 <my-small-loader class="description-loading" [loading]="descriptionLoading"></my-small-loader>
13 </div>
14
15 <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more">
16 <ng-container i18n>Show less</ng-container>
17 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span>
18 </div>
19</div>
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss
new file mode 100644
index 000000000..fc8b4574c
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss
@@ -0,0 +1,46 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.video-info-description {
5 @include margin-left($video-watch-info-margin-left);
6 @include margin-right(0);
7
8 margin-top: 20px;
9 margin-bottom: 20px;
10 font-size: 15px;
11
12 .video-info-description-html {
13 @include peertube-word-wrap;
14
15 ::ng-deep a {
16 text-decoration: none;
17 }
18 }
19
20 .glyphicon,
21 .description-loading {
22 @include margin-left(3px);
23 }
24
25 .description-loading {
26 display: inline-block;
27 }
28
29 .video-info-description-more {
30 cursor: pointer;
31 font-weight: $font-semibold;
32 color: pvar(--greyForegroundColor);
33 font-size: 14px;
34
35 .glyphicon {
36 position: relative;
37 top: 2px;
38 }
39 }
40}
41
42@media screen and (max-width: 450px) {
43 .video-info-description {
44 font-size: 14px !important;
45 }
46}
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts
new file mode 100644
index 000000000..2ea3b206f
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts
@@ -0,0 +1,87 @@
1import { Component, EventEmitter, Inject, Input, LOCALE_ID, OnChanges, Output } from '@angular/core'
2import { MarkdownService, Notifier } from '@app/core'
3import { VideoDetails, VideoService } from '@app/shared/shared-main'
4
5
6@Component({
7 selector: 'my-video-description',
8 templateUrl: './video-description.component.html',
9 styleUrls: [ './video-description.component.scss' ]
10})
11export class VideoDescriptionComponent implements OnChanges {
12 @Input() video: VideoDetails
13
14 @Output() timestampClicked = new EventEmitter<number>()
15
16 descriptionLoading = false
17 completeDescriptionShown = false
18 completeVideoDescription: string
19 shortVideoDescription: string
20 videoHTMLDescription = ''
21
22 constructor (
23 private videoService: VideoService,
24 private notifier: Notifier,
25 private markdownService: MarkdownService,
26 @Inject(LOCALE_ID) private localeId: string
27 ) { }
28
29 ngOnChanges () {
30 this.descriptionLoading = false
31 this.completeDescriptionShown = false
32 this.completeVideoDescription = undefined
33
34 this.setVideoDescriptionHTML()
35 }
36
37 showMoreDescription () {
38 if (this.completeVideoDescription === undefined) {
39 return this.loadCompleteDescription()
40 }
41
42 this.updateVideoDescription(this.completeVideoDescription)
43 this.completeDescriptionShown = true
44 }
45
46 showLessDescription () {
47 this.updateVideoDescription(this.shortVideoDescription)
48 this.completeDescriptionShown = false
49 }
50
51 loadCompleteDescription () {
52 this.descriptionLoading = true
53
54 this.videoService.loadCompleteDescription(this.video.descriptionPath)
55 .subscribe(
56 description => {
57 this.completeDescriptionShown = true
58 this.descriptionLoading = false
59
60 this.shortVideoDescription = this.video.description
61 this.completeVideoDescription = description
62
63 this.updateVideoDescription(this.completeVideoDescription)
64 },
65
66 error => {
67 this.descriptionLoading = false
68 this.notifier.error(error.message)
69 }
70 )
71 }
72
73 onTimestampClicked (timestamp: number) {
74 this.timestampClicked.emit(timestamp)
75 }
76
77 private updateVideoDescription (description: string) {
78 this.video.description = description
79 this.setVideoDescriptionHTML()
80 .catch(err => console.error(err))
81 }
82
83 private async setVideoDescriptionHTML () {
84 const html = await this.markdownService.textMarkdownToHTML(this.video.description)
85 this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html)
86 }
87}
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.html
new file mode 100644
index 000000000..7dd9b3678
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.html
@@ -0,0 +1,23 @@
1<ng-template #ratePopoverText>
2 <span [innerHTML]="getRatePopoverText()"></span>
3</ng-template>
4
5<button
6 [ngbPopover]="getRatePopoverText() && ratePopoverText" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" (keyup.enter)="setLike()"
7 class="action-button action-button-like" [attr.aria-pressed]="userRating === 'like'" [attr.aria-label]="tooltipLike"
8 [ngbTooltip]="tooltipLike"
9 placement="bottom auto"
10>
11 <my-global-icon iconName="like"></my-global-icon>
12 <span *ngIf="video.likes" class="count">{{ video.likes }}</span>
13</button>
14
15<button
16 [ngbPopover]="getRatePopoverText() && ratePopoverText" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" (keyup.enter)="setDislike()"
17 class="action-button action-button-dislike" [attr.aria-pressed]="userRating === 'dislike'" [attr.aria-label]="tooltipDislike"
18 [ngbTooltip]="tooltipDislike"
19 placement="bottom auto"
20>
21 <my-global-icon iconName="dislike"></my-global-icon>
22 <span *ngIf="video.dislikes" class="count">{{ video.dislikes }}</span>
23</button>
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.scss
new file mode 100644
index 000000000..f4f696f33
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.scss
@@ -0,0 +1,15 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.action-button-like,
5.action-button-dislike {
6 filter: brightness(120%);
7
8 .count {
9 margin: 0 5px;
10 }
11}
12
13.activated {
14 color: pvar(--activatedActionButtonColor) !important;
15}
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.ts b/client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.ts
new file mode 100644
index 000000000..89a666a62
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-rate.component.ts
@@ -0,0 +1,142 @@
1import { Hotkey, HotkeysService } from 'angular2-hotkeys'
2import { Observable } from 'rxjs'
3import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'
4import { Notifier, ScreenService } from '@app/core'
5import { VideoDetails, VideoService } from '@app/shared/shared-main'
6import { UserVideoRateType } from '@shared/models'
7
8@Component({
9 selector: 'my-video-rate',
10 templateUrl: './video-rate.component.html',
11 styleUrls: [ './video-rate.component.scss' ]
12})
13export class VideoRateComponent implements OnInit, OnChanges, OnDestroy {
14 @Input() video: VideoDetails
15 @Input() isUserLoggedIn: boolean
16
17 @Output() userRatingLoaded = new EventEmitter<UserVideoRateType>()
18 @Output() rateUpdated = new EventEmitter<UserVideoRateType>()
19
20 userRating: UserVideoRateType
21
22 tooltipLike = ''
23 tooltipDislike = ''
24
25 private hotkeys: Hotkey[]
26
27 constructor (
28 private videoService: VideoService,
29 private notifier: Notifier,
30 private hotkeysService: HotkeysService,
31 private screenService: ScreenService
32 ) { }
33
34 async ngOnInit () {
35 // Hide the tooltips for unlogged users in mobile view, this adds confusion with the popover
36 if (this.isUserLoggedIn || !this.screenService.isInMobileView()) {
37 this.tooltipLike = $localize`Like this video`
38 this.tooltipDislike = $localize`Dislike this video`
39 }
40
41 if (this.isUserLoggedIn) {
42 this.hotkeys = [
43 new Hotkey('shift+l', () => {
44 this.setLike()
45 return false
46 }, undefined, $localize`Like the video`),
47
48 new Hotkey('shift+d', () => {
49 this.setDislike()
50 return false
51 }, undefined, $localize`Dislike the video`)
52 ]
53
54 this.hotkeysService.add(this.hotkeys)
55 }
56 }
57
58 ngOnChanges () {
59 this.checkUserRating()
60 }
61
62 ngOnDestroy () {
63 this.hotkeysService.remove(this.hotkeys)
64 }
65
66 setLike () {
67 if (this.isUserLoggedIn === false) return
68
69 // Already liked this video
70 if (this.userRating === 'like') this.setRating('none')
71 else this.setRating('like')
72 }
73
74 setDislike () {
75 if (this.isUserLoggedIn === false) return
76
77 // Already disliked this video
78 if (this.userRating === 'dislike') this.setRating('none')
79 else this.setRating('dislike')
80 }
81
82 getRatePopoverText () {
83 if (this.isUserLoggedIn) return undefined
84
85 return $localize`You need to be <a href="/login">logged in</a> to rate this video.`
86 }
87
88 private checkUserRating () {
89 // Unlogged users do not have ratings
90 if (this.isUserLoggedIn === false) return
91
92 this.videoService.getUserVideoRating(this.video.id)
93 .subscribe(
94 ratingObject => {
95 if (!ratingObject) return
96
97 this.userRating = ratingObject.rating
98 this.userRatingLoaded.emit(this.userRating)
99 },
100
101 err => this.notifier.error(err.message)
102 )
103 }
104
105 private setRating (nextRating: UserVideoRateType) {
106 const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable<any> } = {
107 like: this.videoService.setVideoLike,
108 dislike: this.videoService.setVideoDislike,
109 none: this.videoService.unsetVideoLike
110 }
111
112 ratingMethods[nextRating].call(this.videoService, this.video.id)
113 .subscribe(
114 () => {
115 // Update the video like attribute
116 this.updateVideoRating(this.userRating, nextRating)
117 this.userRating = nextRating
118 this.rateUpdated.emit(this.userRating)
119 },
120
121 (err: { message: string }) => this.notifier.error(err.message)
122 )
123 }
124
125 private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
126 let likesToIncrement = 0
127 let dislikesToIncrement = 0
128
129 if (oldRating) {
130 if (oldRating === 'like') likesToIncrement--
131 if (oldRating === 'dislike') dislikesToIncrement--
132 }
133
134 if (newRating === 'like') likesToIncrement++
135 if (newRating === 'dislike') dislikesToIncrement++
136
137 this.video.likes += likesToIncrement
138 this.video.dislikes += dislikesToIncrement
139
140 this.video.buildLikeAndDislikePercents()
141 }
142}