diff options
Diffstat (limited to 'client/src/app/+videos/+video-watch/shared/action-buttons')
7 files changed, 459 insertions, 0 deletions
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html new file mode 100644 index 000000000..8fb244cc4 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html | |||
@@ -0,0 +1,85 @@ | |||
1 | <div class="video-actions-rates"> | ||
2 | <div class="video-actions full-width justify-content-end"> | ||
3 | <my-video-rate | ||
4 | [video]="video" [isUserLoggedIn]="isUserLoggedIn" | ||
5 | (rateUpdated)="onRateUpdated($event)" (userRatingLoaded)="onRateUpdated($event)" | ||
6 | ></my-video-rate> | ||
7 | |||
8 | <button *ngIf="video.support" (click)="showSupportModal()" (keyup.enter)="showSupportModal()" class="action-button action-button-support" [attr.aria-label]="tooltipSupport" | ||
9 | [ngbTooltip]="tooltipSupport" | ||
10 | placement="bottom auto" | ||
11 | > | ||
12 | <my-global-icon iconName="support" aria-hidden="true"></my-global-icon> | ||
13 | <span class="icon-text" i18n>SUPPORT</span> | ||
14 | </button> | ||
15 | |||
16 | <button (click)="showShareModal()" (keyup.enter)="showShareModal()" class="action-button"> | ||
17 | <my-global-icon iconName="share" aria-hidden="true"></my-global-icon> | ||
18 | <span class="icon-text" i18n>SHARE</span> | ||
19 | </button> | ||
20 | |||
21 | <div | ||
22 | class="action-dropdown" ngbDropdown placement="top" role="button" autoClose="outside" | ||
23 | *ngIf="isUserLoggedIn" (openChange)="addContent.openChange($event)" | ||
24 | [ngbTooltip]="tooltipSaveToPlaylist" | ||
25 | placement="bottom auto" | ||
26 | > | ||
27 | <button class="action-button action-button-save" ngbDropdownToggle> | ||
28 | <my-global-icon iconName="playlist-add" aria-hidden="true"></my-global-icon> | ||
29 | <span class="icon-text" i18n>SAVE</span> | ||
30 | </button> | ||
31 | |||
32 | <div ngbDropdownMenu> | ||
33 | <my-video-add-to-playlist #addContent [video]="video"></my-video-add-to-playlist> | ||
34 | </div> | ||
35 | </div> | ||
36 | |||
37 | <ng-container *ngIf="!isUserLoggedIn && !video.isLive"> | ||
38 | <button | ||
39 | *ngIf="isVideoDownloadable()" class="action-button action-button-save" | ||
40 | (click)="showDownloadModal()" (keydown.enter)="showDownloadModal()" | ||
41 | > | ||
42 | <my-global-icon iconName="download" aria-hidden="true"></my-global-icon> | ||
43 | <span class="icon-text d-none d-sm-inline" i18n>DOWNLOAD</span> | ||
44 | </button> | ||
45 | |||
46 | <my-video-download #videoDownloadModal></my-video-download> | ||
47 | </ng-container> | ||
48 | |||
49 | <ng-container *ngIf="isUserLoggedIn"> | ||
50 | <my-video-actions-dropdown | ||
51 | placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions" | ||
52 | [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" | ||
53 | ></my-video-actions-dropdown> | ||
54 | </ng-container> | ||
55 | </div> | ||
56 | |||
57 | <div class="likes-dislikes-bar-outer-container"> | ||
58 | <div | ||
59 | class="likes-dislikes-bar-inner-container" | ||
60 | *ngIf="video.likes !== 0 || video.dislikes !== 0" | ||
61 | [ngbTooltip]="likesBarTooltipText" | ||
62 | placement="bottom" | ||
63 | > | ||
64 | <div | ||
65 | class="likes-dislikes-bar" | ||
66 | > | ||
67 | <div class="likes-bar" [ngClass]="{ 'liked': userRating !== 'none' }" [ngStyle]="{ 'width.%': video.likesPercent }"></div> | ||
68 | </div> | ||
69 | </div> | ||
70 | </div> | ||
71 | </div> | ||
72 | |||
73 | <div | ||
74 | class="likes-dislikes-bar" | ||
75 | *ngIf="video.likes !== 0 || video.dislikes !== 0" | ||
76 | [ngbTooltip]="likesBarTooltipText" | ||
77 | placement="bottom" | ||
78 | > | ||
79 | <div class="likes-bar" [ngStyle]="{ 'width.%': video.likesPercent }"></div> | ||
80 | </div> | ||
81 | |||
82 | <ng-container *ngIf="video"> | ||
83 | <my-support-modal #supportModal [video]="video"></my-support-modal> | ||
84 | <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share> | ||
85 | </ng-container> | ||
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss new file mode 100644 index 000000000..967d515e6 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss | |||
@@ -0,0 +1,99 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | .video-actions { | ||
5 | height: 40px; // Align with the title | ||
6 | display: flex; | ||
7 | align-items: center; | ||
8 | |||
9 | .action-button:not(:first-child), | ||
10 | .action-dropdown, | ||
11 | my-video-actions-dropdown { | ||
12 | @include margin-left(5px); | ||
13 | } | ||
14 | |||
15 | ::ng-deep.action-button { | ||
16 | @include peertube-button; | ||
17 | @include button-with-icon(21px, 0, -1px); | ||
18 | |||
19 | font-size: 100%; | ||
20 | font-weight: $font-semibold; | ||
21 | display: inline-block; | ||
22 | padding: 0 10px; | ||
23 | white-space: nowrap; | ||
24 | background-color: transparent !important; | ||
25 | color: pvar(--actionButtonColor); | ||
26 | text-transform: uppercase; | ||
27 | |||
28 | &::after { | ||
29 | display: none; | ||
30 | } | ||
31 | |||
32 | &:hover { | ||
33 | opacity: 0.9; | ||
34 | } | ||
35 | |||
36 | &.action-button-support { | ||
37 | color: pvar(--supportButtonColor); | ||
38 | |||
39 | my-global-icon { | ||
40 | @include apply-svg-color(pvar(--supportButtonColor)); | ||
41 | } | ||
42 | } | ||
43 | |||
44 | &.action-button-support { | ||
45 | my-global-icon { | ||
46 | ::ng-deep path:first-child { | ||
47 | fill: pvar(--supportButtonHeartColor) !important; | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | |||
52 | &.action-button-save { | ||
53 | my-global-icon { | ||
54 | top: 0 !important; | ||
55 | right: -1px; | ||
56 | } | ||
57 | } | ||
58 | |||
59 | .icon-text { | ||
60 | @include margin-left(3px); | ||
61 | } | ||
62 | } | ||
63 | } | ||
64 | |||
65 | .likes-dislikes-bar-outer-container { | ||
66 | position: relative; | ||
67 | } | ||
68 | |||
69 | .likes-dislikes-bar-inner-container { | ||
70 | position: absolute; | ||
71 | height: 20px; | ||
72 | } | ||
73 | |||
74 | .likes-dislikes-bar { | ||
75 | $likes-bar-height: 2px; | ||
76 | |||
77 | height: $likes-bar-height; | ||
78 | margin-top: -$likes-bar-height; | ||
79 | |||
80 | width: 120px; | ||
81 | background-color: #ccc; | ||
82 | position: relative; | ||
83 | top: 10px; | ||
84 | |||
85 | .likes-bar { | ||
86 | height: 100%; | ||
87 | background-color: #909090; | ||
88 | |||
89 | &.liked { | ||
90 | background-color: pvar(--activatedActionButtonColor); | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | |||
95 | @media screen and (max-width: 450px) { | ||
96 | .action-button .icon-text { | ||
97 | display: none !important; | ||
98 | } | ||
99 | } | ||
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts new file mode 100644 index 000000000..e59238ffe --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts | |||
@@ -0,0 +1,93 @@ | |||
1 | import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core' | ||
2 | import { RedirectService, ScreenService } from '@app/core' | ||
3 | import { VideoDetails } from '@app/shared/shared-main' | ||
4 | import { VideoShareComponent } from '@app/shared/shared-share-modal' | ||
5 | import { SupportModalComponent } from '@app/shared/shared-support-modal' | ||
6 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' | ||
7 | import { VideoPlaylist } from '@app/shared/shared-video-playlist' | ||
8 | import { UserVideoRateType, VideoCaption } from '@shared/models/videos' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-action-buttons', | ||
12 | templateUrl: './action-buttons.component.html', | ||
13 | styleUrls: [ './action-buttons.component.scss' ] | ||
14 | }) | ||
15 | export class ActionButtonsComponent implements OnInit, OnChanges { | ||
16 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent | ||
17 | @ViewChild('supportModal') supportModal: SupportModalComponent | ||
18 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent | ||
19 | |||
20 | @Input() video: VideoDetails | ||
21 | @Input() videoCaptions: VideoCaption[] | ||
22 | @Input() playlist: VideoPlaylist | ||
23 | |||
24 | @Input() isUserLoggedIn: boolean | ||
25 | |||
26 | @Input() currentTime: number | ||
27 | @Input() currentPlaylistPosition: number | ||
28 | |||
29 | likesBarTooltipText = '' | ||
30 | |||
31 | tooltipSupport = '' | ||
32 | tooltipSaveToPlaylist = '' | ||
33 | |||
34 | videoActionsOptions: VideoActionsDisplayType = { | ||
35 | playlist: false, | ||
36 | download: true, | ||
37 | update: true, | ||
38 | blacklist: true, | ||
39 | delete: true, | ||
40 | report: true, | ||
41 | duplicate: true, | ||
42 | mute: true, | ||
43 | liveInfo: true | ||
44 | } | ||
45 | |||
46 | userRating: UserVideoRateType | ||
47 | |||
48 | constructor ( | ||
49 | private screenService: ScreenService, | ||
50 | private redirectService: RedirectService | ||
51 | ) { } | ||
52 | |||
53 | ngOnInit () { | ||
54 | // Hide the tooltips for unlogged users in mobile view, this adds confusion with the popover | ||
55 | if (this.isUserLoggedIn || !this.screenService.isInMobileView()) { | ||
56 | this.tooltipSupport = $localize`Support options for this video` | ||
57 | this.tooltipSaveToPlaylist = $localize`Save to playlist` | ||
58 | } | ||
59 | } | ||
60 | |||
61 | ngOnChanges () { | ||
62 | this.setVideoLikesBarTooltipText() | ||
63 | } | ||
64 | |||
65 | showDownloadModal () { | ||
66 | this.videoDownloadModal.show(this.video, this.videoCaptions) | ||
67 | } | ||
68 | |||
69 | isVideoDownloadable () { | ||
70 | return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled && !this.video.isLive | ||
71 | } | ||
72 | |||
73 | showSupportModal () { | ||
74 | this.supportModal.show() | ||
75 | } | ||
76 | |||
77 | showShareModal () { | ||
78 | this.videoShareModal.show(this.currentTime, this.currentPlaylistPosition) | ||
79 | } | ||
80 | |||
81 | onRateUpdated (userRating: UserVideoRateType) { | ||
82 | this.userRating = userRating | ||
83 | this.setVideoLikesBarTooltipText() | ||
84 | } | ||
85 | |||
86 | onVideoRemoved () { | ||
87 | this.redirectService.redirectToHomepage() | ||
88 | } | ||
89 | |||
90 | private setVideoLikesBarTooltipText () { | ||
91 | this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes` | ||
92 | } | ||
93 | } | ||
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/index.ts b/client/src/app/+videos/+video-watch/shared/action-buttons/index.ts new file mode 100644 index 000000000..3844dd12e --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './action-buttons.component' | ||
2 | export * from './video-rate.component' | ||
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.html b/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.html new file mode 100644 index 000000000..7dd9b3678 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/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/action-buttons/video-rate.component.scss b/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.scss new file mode 100644 index 000000000..f4f696f33 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/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/action-buttons/video-rate.component.ts b/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts new file mode 100644 index 000000000..ecb5a9281 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/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 | 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 | } | ||