diff options
author | Chocobozzz <me@florianbigard.com> | 2021-06-29 17:18:30 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-06-29 17:18:39 +0200 |
commit | 911186dae411d78788ccede093c251303187589a (patch) | |
tree | 967a07cd985ae4e2ea5249855726455fe929471d /client/src/app/+videos/+video-watch/shared/metadata | |
parent | b0c43e36dbdc2c964f6828a78b146faebfb75b21 (diff) | |
download | PeerTube-911186dae411d78788ccede093c251303187589a.tar.gz PeerTube-911186dae411d78788ccede093c251303187589a.tar.zst PeerTube-911186dae411d78788ccede093c251303187589a.zip |
Reorganize watch components
Diffstat (limited to 'client/src/app/+videos/+video-watch/shared/metadata')
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 @@ | |||
1 | export * from './video-avatar-channel.component' | ||
2 | export * from './video-description.component' | ||
3 | export * 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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { 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 | }) | ||
9 | export 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 @@ | |||
1 | import { Component, EventEmitter, Inject, Input, LOCALE_ID, OnChanges, Output } from '@angular/core' | ||
2 | import { MarkdownService, Notifier } from '@app/core' | ||
3 | import { 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 | }) | ||
11 | export 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 @@ | |||
1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | ||
2 | import { Observable } from 'rxjs' | ||
3 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core' | ||
4 | import { Notifier, ScreenService } from '@app/core' | ||
5 | import { VideoDetails, VideoService } from '@app/shared/shared-main' | ||
6 | import { 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 | }) | ||
13 | export 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 | } | ||