diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html index 4b94490a0..6e274f689 100644 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.html +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.html @@ -15,6 +15,8 @@
- +
diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html index da2ace54d..0a9f78cb2 100644 --- a/client/src/app/search/search.component.html +++ b/client/src/app/search/search.component.html @@ -48,7 +48,10 @@
- +
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts index a3383ed8a..a7ddbe1f8 100644 --- a/client/src/app/search/search.component.ts +++ b/client/src/app/search/search.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, Notifier, ServerService } from '@app/core' +import { AuthService, Notifier } from '@app/core' import { forkJoin, Subscription } from 'rxjs' import { SearchService } from '@app/search/search.service' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' @@ -138,6 +138,10 @@ export class SearchComponent implements OnInit, OnDestroy { return this.advancedSearch.size() } + removeVideoFromArray (video: Video) { + this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) + } + private resetPagination () { this.pagination.currentPage = 1 this.pagination.totalItems = null diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html index 6999474d6..cc244dc76 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.html +++ b/client/src/app/shared/buttons/action-dropdown.component.html @@ -1,9 +1,11 @@ diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss index 985b2ca88..5073190b0 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.scss +++ b/client/src/app/shared/buttons/action-dropdown.component.scss @@ -8,12 +8,19 @@ .action-button { @include peertube-button; - &.grey { - @include grey-button; - } + &.button-styled { - &.orange { - @include orange-button; + &.grey { + @include grey-button; + } + + &.orange { + @include orange-button; + } + + &:hover, &:active, &:focus { + background-color: $grey-background-color; + } } display: inline-block; @@ -23,10 +30,6 @@ display: none; } - &:hover, &:active, &:focus { - background-color: $grey-background-color; - } - .more-icon { width: 21px; } @@ -48,6 +51,10 @@ cursor: pointer; color: #000 !important; + &.with-icon { + @include dropdown-with-icon-item; + } + a, span { display: block; width: 100%; diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts index 275e2b51e..f5345831b 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.ts +++ b/client/src/app/shared/buttons/action-dropdown.component.ts @@ -1,12 +1,18 @@ import { Component, Input } from '@angular/core' +import { GlobalIconName } from '@app/shared/images/global-icon.component' export type DropdownAction = { label?: string + iconName?: GlobalIconName handler?: (a: T) => any linkBuilder?: (a: T) => (string | number)[] isDisplayed?: (a: T) => boolean } +export type DropdownButtonSize = 'normal' | 'small' +export type DropdownTheme = 'orange' | 'grey' +export type DropdownDirection = 'horizontal' | 'vertical' + @Component({ selector: 'my-action-dropdown', styleUrls: [ './action-dropdown.component.scss' ], @@ -16,14 +22,29 @@ export type DropdownAction = { export class ActionDropdownComponent { @Input() actions: DropdownAction[] | DropdownAction[][] = [] @Input() entry: T + @Input() placement = 'bottom-left' - @Input() buttonSize: 'normal' | 'small' = 'normal' + + @Input() buttonSize: DropdownButtonSize = 'normal' + @Input() buttonDirection: DropdownDirection = 'horizontal' + @Input() buttonStyled = true + @Input() label: string - @Input() theme: 'orange' | 'grey' = 'grey' + @Input() theme: DropdownTheme = 'grey' getActions () { if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions return [ this.actions ] } + + areActionsDisplayed (actions: DropdownAction[], entry: T) { + return actions.some(a => a.isDisplayed === undefined || a.isDisplayed(entry)) + } + + handleClick (event: Event, action: DropdownAction) { + event.preventDefault() + + // action.handler(entry) + } } diff --git a/client/src/app/shared/misc/screen.service.ts b/client/src/app/shared/misc/screen.service.ts index 1cbc96b14..db481204e 100644 --- a/client/src/app/shared/misc/screen.service.ts +++ b/client/src/app/shared/misc/screen.service.ts @@ -32,6 +32,8 @@ export class ScreenService { } private cacheWindowInnerWidthExpired () { + if (!this.lastFunctionCallTime) return true + return new Date().getTime() > (this.lastFunctionCallTime + this.cacheForMs) } } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 68225b457..ded65653f 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -80,6 +80,11 @@ import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe' import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' import { FromNowPipe } from '@app/shared/angular/from-now.pipe' import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' +import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component' +import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' +import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' +import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' +import { ClipboardModule } from 'ngx-clipboard' @NgModule({ imports: [ @@ -95,6 +100,8 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template NgbTabsetModule, NgbTooltipModule, + ClipboardModule, + PrimeSharedModule, InputMaskModule, NgPipesModule @@ -110,6 +117,11 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template VideoAddToPlaylistComponent, VideoPlaylistElementMiniatureComponent, VideosSelectionComponent, + VideoActionsDropdownComponent, + + VideoDownloadComponent, + VideoReportComponent, + VideoBlacklistComponent, FeedComponent, @@ -158,6 +170,8 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template NgbTabsetModule, NgbTooltipModule, + ClipboardModule, + PrimeSharedModule, InputMaskModule, BytesPipe, @@ -172,6 +186,11 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template VideoAddToPlaylistComponent, VideoPlaylistElementMiniatureComponent, VideosSelectionComponent, + VideoActionsDropdownComponent, + + VideoDownloadComponent, + VideoReportComponent, + VideoBlacklistComponent, FeedComponent, diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html index 19b326206..6029b3648 100644 --- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html +++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html @@ -1,74 +1,76 @@ -
-
-
Save to
+
+
+
+
Save to
-
- +
+ - Options + Options +
+
+ +
+
+ + + +
+ +
+ + + +
-
-
- + +
+ {{ playlist.displayName }} -
- - - +
+ {{ formatTimestamp(playlist) }} +
+ + + +
- - - - - - diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss b/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss index bc0d55912..0424e2ee9 100644 --- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss +++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss @@ -1,6 +1,11 @@ @import '_variables'; @import '_mixins'; +.root { + max-height: 300px; + overflow-y: auto; +} + .header { min-width: 240px; padding: 6px 24px 10px 24px; diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts index 705f62404..152f20c85 100644 --- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts +++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts @@ -24,6 +24,7 @@ type PlaylistSummary = { export class VideoAddToPlaylistComponent extends FormReactive implements OnInit { @Input() video: Video @Input() currentVideoTimestamp: number + @Input() lazyLoad = false isNewPlaylistBlockOpened = false videoPlaylists: PlaylistSummary[] = [] @@ -57,6 +58,10 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME }) + if (this.lazyLoad !== true) this.load() + } + + load () { forkJoin([ this.videoPlaylistService.listAccountPlaylists(this.user.account, '-updatedAt'), this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id) diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index e134654a3..d1b761674 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html @@ -1,4 +1,4 @@ -
+
@@ -11,7 +11,7 @@
@@ -22,7 +22,11 @@ myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos" > - +
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 099650129..cf43d429d 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts @@ -26,11 +26,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor syndicationItems: Syndication[] = [] loadOnInit = true - marginContent = true videos: Video[] = [] ownerDisplayType: OwnerDisplayType = 'account' displayModerationBlock = false titleTooltip: string + displayVideoActions = true disabled = false @@ -120,6 +120,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor throw new Error('toggleModerationDisplay is not implemented') } + removeVideoFromArray (video: Video) { + this.videos = this.videos.filter(v => v.id !== video.id) + } + // On videos hook for children that want to do something protected onMoreVideos () { /* empty */ } diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/shared/video/modals/video-blacklist.component.html similarity index 100% rename from client/src/app/videos/+video-watch/modal/video-blacklist.component.html rename to client/src/app/shared/video/modals/video-blacklist.component.html diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.scss b/client/src/app/shared/video/modals/video-blacklist.component.scss similarity index 100% rename from client/src/app/videos/+video-watch/modal/video-blacklist.component.scss rename to client/src/app/shared/video/modals/video-blacklist.component.scss diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts b/client/src/app/shared/video/modals/video-blacklist.component.ts similarity index 82% rename from client/src/app/videos/+video-watch/modal/video-blacklist.component.ts rename to client/src/app/shared/video/modals/video-blacklist.component.ts index 50a7cadd1..4e4e8dc50 100644 --- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts +++ b/client/src/app/shared/video/modals/video-blacklist.component.ts @@ -1,11 +1,12 @@ -import { Component, Input, OnInit, ViewChild } from '@angular/core' +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' import { Notifier, RedirectService } from '@app/core' -import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index' +import { VideoBlacklistService } from '../../../shared/video-blacklist' import { VideoDetails } from '../../../shared/video/video-details.model' import { I18n } from '@ngx-translate/i18n-polyfill' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' +import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms' @Component({ selector: 'my-video-blacklist', @@ -17,6 +18,8 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit { @ViewChild('modal') modal: NgbModal + @Output() videoBlacklisted = new EventEmitter() + error: string = null private openedModal: NgbModalRef @@ -60,7 +63,11 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit { () => { this.notifier.success(this.i18n('Video blacklisted.')) this.hide() - this.redirectService.redirectToHomepage() + + this.video.blacklisted = true + this.video.blacklistedReason = reason + + this.videoBlacklisted.emit() }, err => this.notifier.error(err.message) diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/shared/video/modals/video-download.component.html similarity index 100% rename from client/src/app/videos/+video-watch/modal/video-download.component.html rename to client/src/app/shared/video/modals/video-download.component.html diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.scss b/client/src/app/shared/video/modals/video-download.component.scss similarity index 100% rename from client/src/app/videos/+video-watch/modal/video-download.component.scss rename to client/src/app/shared/video/modals/video-download.component.scss diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts similarity index 67% rename from client/src/app/videos/+video-watch/modal/video-download.component.ts rename to client/src/app/shared/video/modals/video-download.component.ts index 834385771..64aaeb3c8 100644 --- a/client/src/app/videos/+video-watch/modal/video-download.component.ts +++ b/client/src/app/shared/video/modals/video-download.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core' +import { Component, ElementRef, ViewChild } from '@angular/core' import { VideoDetails } from '../../../shared/video/video-details.model' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { I18n } from '@ngx-translate/i18n-polyfill' @@ -9,26 +9,32 @@ import { Notifier } from '@app/core' templateUrl: './video-download.component.html', styleUrls: [ './video-download.component.scss' ] }) -export class VideoDownloadComponent implements OnInit { - @Input() video: VideoDetails = null - +export class VideoDownloadComponent { @ViewChild('modal') modal: ElementRef downloadType: 'direct' | 'torrent' | 'magnet' = 'torrent' resolutionId: number | string = -1 + private video: VideoDetails + constructor ( private notifier: Notifier, private modalService: NgbModal, private i18n: I18n ) { } - ngOnInit () { + show (video: VideoDetails) { + this.video = video + + const m = this.modalService.open(this.modal) + m.result.then(() => this.onClose()) + .catch(() => this.onClose()) + this.resolutionId = this.video.files[0].resolution.id } - show () { - this.modalService.open(this.modal) + onClose () { + this.video = undefined } download () { @@ -45,21 +51,16 @@ export class VideoDownloadComponent implements OnInit { return } - const link = (() => { - switch (this.downloadType) { - case 'direct': { - return file.fileDownloadUrl - } - case 'torrent': { - return file.torrentDownloadUrl - } - case 'magnet': { - return file.magnetUri - } - } - })() + switch (this.downloadType) { + case 'direct': + return file.fileDownloadUrl - return link + case 'torrent': + return file.torrentDownloadUrl + + case 'magnet': + return file.magnetUri + } } activateCopiedMessage () { diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/shared/video/modals/video-report.component.html similarity index 100% rename from client/src/app/videos/+video-watch/modal/video-report.component.html rename to client/src/app/shared/video/modals/video-report.component.html diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.scss b/client/src/app/shared/video/modals/video-report.component.scss similarity index 100% rename from client/src/app/videos/+video-watch/modal/video-report.component.scss rename to client/src/app/shared/video/modals/video-report.component.scss diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts similarity index 95% rename from client/src/app/videos/+video-watch/modal/video-report.component.ts rename to client/src/app/shared/video/modals/video-report.component.ts index 911f3b447..725dd020f 100644 --- a/client/src/app/videos/+video-watch/modal/video-report.component.ts +++ b/client/src/app/shared/video/modals/video-report.component.ts @@ -1,12 +1,13 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core' import { Notifier } from '@app/core' -import { FormReactive, VideoAbuseService } from '../../../shared/index' +import { FormReactive } from '../../../shared/forms' import { VideoDetails } from '../../../shared/video/video-details.model' import { I18n } from '@ngx-translate/i18n-polyfill' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' +import { VideoAbuseService } from '@app/shared/video-abuse' @Component({ selector: 'my-video-report', diff --git a/client/src/app/shared/video/video-actions-dropdown.component.html b/client/src/app/shared/video/video-actions-dropdown.component.html new file mode 100644 index 000000000..300fe318a --- /dev/null +++ b/client/src/app/shared/video/video-actions-dropdown.component.html @@ -0,0 +1,21 @@ + + +
+ + +
+ +
+
+ + + + + + +
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.scss b/client/src/app/shared/video/video-actions-dropdown.component.scss new file mode 100644 index 000000000..7ffdce822 --- /dev/null +++ b/client/src/app/shared/video/video-actions-dropdown.component.scss @@ -0,0 +1,12 @@ +.playlist-dropdown { + position: absolute; + + .anchor { + display: block; + opacity: 0; + } +} + +/deep/ .icon-playlist-add { + left: 2px; +} diff --git a/client/src/app/shared/video/video-actions-dropdown.component.ts b/client/src/app/shared/video/video-actions-dropdown.component.ts new file mode 100644 index 000000000..90bdf7df8 --- /dev/null +++ b/client/src/app/shared/video/video-actions-dropdown.component.ts @@ -0,0 +1,237 @@ +import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { DropdownAction, DropdownButtonSize, DropdownDirection } from '@app/shared/buttons/action-dropdown.component' +import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' +import { BlocklistService } from '@app/shared/blocklist' +import { Video } from '@app/shared/video/video.model' +import { VideoService } from '@app/shared/video/video.service' +import { VideoDetails } from '@app/shared/video/video-details.model' +import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' +import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' +import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' +import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' +import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' +import { VideoBlacklistService } from '@app/shared/video-blacklist' +import { ScreenService } from '@app/shared/misc/screen.service' + +export type VideoActionsDisplayType = { + playlist?: boolean + download?: boolean + update?: boolean + blacklist?: boolean + delete?: boolean + report?: boolean +} + +@Component({ + selector: 'my-video-actions-dropdown', + templateUrl: './video-actions-dropdown.component.html', + styleUrls: [ './video-actions-dropdown.component.scss' ] +}) +export class VideoActionsDropdownComponent implements OnChanges { + @ViewChild('playlistDropdown') playlistDropdown: NgbDropdown + @ViewChild('playlistAdd') playlistAdd: VideoAddToPlaylistComponent + + @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent + @ViewChild('videoReportModal') videoReportModal: VideoReportComponent + @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent + + @Input() video: Video | VideoDetails + + @Input() displayOptions: VideoActionsDisplayType = { + playlist: false, + download: true, + update: true, + blacklist: true, + delete: true, + report: true + } + @Input() placement: string = 'left' + + @Input() label: string + + @Input() buttonStyled = false + @Input() buttonSize: DropdownButtonSize = 'normal' + @Input() buttonDirection: DropdownDirection = 'vertical' + + @Output() videoRemoved = new EventEmitter() + @Output() videoUnblacklisted = new EventEmitter() + @Output() videoBlacklisted = new EventEmitter() + + videoActions: DropdownAction<{ video: Video }>[][] = [] + + private loaded = false + + constructor ( + private authService: AuthService, + private notifier: Notifier, + private confirmService: ConfirmService, + private videoBlacklistService: VideoBlacklistService, + private serverService: ServerService, + private screenService: ScreenService, + private videoService: VideoService, + private blocklistService: BlocklistService, + private i18n: I18n + ) { } + + get user () { + return this.authService.getUser() + } + + ngOnChanges () { + this.buildActions() + } + + isUserLoggedIn () { + return this.authService.isLoggedIn() + } + + loadDropdownInformation () { + if (!this.isUserLoggedIn() || this.loaded === true) return + + this.loaded = true + + if (this.displayOptions.playlist) this.playlistAdd.load() + } + + /* Show modals */ + + showDownloadModal () { + this.videoDownloadModal.show(this.video as VideoDetails) + } + + showReportModal () { + this.videoReportModal.show() + } + + showBlacklistModal () { + this.videoBlacklistModal.show() + } + + /* Actions checker */ + + isVideoUpdatable () { + return this.video.isUpdatableBy(this.user) + } + + isVideoRemovable () { + return this.video.isRemovableBy(this.user) + } + + isVideoBlacklistable () { + return this.video.isBlackistableBy(this.user) + } + + isVideoUnblacklistable () { + return this.video.isUnblacklistableBy(this.user) + } + + /* Action handlers */ + + async unblacklistVideo () { + const confirmMessage = this.i18n( + 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.' + ) + + const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist')) + if (res === false) return + + this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe( + () => { + this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name })) + + this.video.blacklisted = false + this.video.blacklistedReason = null + + this.videoUnblacklisted.emit() + }, + + err => this.notifier.error(err.message) + ) + } + + async removeVideo () { + const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete')) + if (res === false) return + + this.videoService.removeVideo(this.video.id) + .subscribe( + () => { + this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })) + + this.videoRemoved.emit() + }, + + error => this.notifier.error(error.message) + ) + } + + onVideoBlacklisted () { + this.videoBlacklisted.emit() + } + + getPlaylistDropdownPlacement () { + if (this.screenService.isInSmallView()) { + return 'bottom-right' + } + + return 'bottom-left bottom-right' + } + + private buildActions () { + this.videoActions = [] + + if (this.authService.isLoggedIn()) { + this.videoActions.push([ + { + label: this.i18n('Save to playlist'), + handler: () => this.playlistDropdown.toggle(), + isDisplayed: () => this.displayOptions.playlist, + iconName: 'playlist-add' + } + ]) + + this.videoActions.push([ + { + label: this.i18n('Download'), + handler: () => this.showDownloadModal(), + isDisplayed: () => this.displayOptions.download, + iconName: 'download' + }, + { + label: this.i18n('Update'), + linkBuilder: ({ video }) => [ '/videos/update', video.uuid ], + iconName: 'edit', + isDisplayed: () => this.displayOptions.update && this.isVideoUpdatable() + }, + { + label: this.i18n('Blacklist'), + handler: () => this.showBlacklistModal(), + iconName: 'no', + isDisplayed: () => this.displayOptions.blacklist && this.isVideoBlacklistable() + }, + { + label: this.i18n('Unblacklist'), + handler: () => this.unblacklistVideo(), + iconName: 'undo', + isDisplayed: () => this.displayOptions.blacklist && this.isVideoUnblacklistable() + }, + { + label: this.i18n('Delete'), + handler: () => this.removeVideo(), + isDisplayed: () => this.displayOptions.delete && this.isVideoRemovable(), + iconName: 'delete' + } + ]) + + this.videoActions.push([ + { + label: this.i18n('Report'), + handler: () => this.showReportModal(), + isDisplayed: () => this.displayOptions.report, + iconName: 'alert' + } + ]) + } + } +} diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 388357343..8463e15d7 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -44,22 +44,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { this.buildLikeAndDislikePercents() } - isRemovableBy (user: AuthUser) { - return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) - } - - isBlackistableBy (user: AuthUser) { - return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true - } - - isUnblacklistableBy (user: AuthUser) { - return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true - } - - isUpdatableBy (user: AuthUser) { - return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) - } - buildLikeAndDislikePercents () { this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html index f4ae0b0dd..7af0f1113 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html @@ -1,47 +1,56 @@ -
+
-
- - - Unlisted - Private - +
+
+ + + Unlisted + Private + - {{ video.name }} - + {{ video.name }} + - - {{ video.publishedAt | myFromNow }} - - - {{ video.views | myNumberFormatter }} views - + + {{ video.publishedAt | myFromNow }} + - + {{ video.views | myNumberFormatter }} views + - - - {{ video.byVideoChannel }} - + + + {{ video.byVideoChannel }} + -
- {{ video.privacy.label }} - - - {{ getStateLabel(video) }} +
+ {{ video.privacy.label }} + - + {{ getStateLabel(video) }} +
+ +
+ Blacklisted + {{ video.blacklistedReason }} +
+ +
+ Sensitive +
-
- Blacklisted - {{ video.blacklistedReason }} +
+ +
- -
- Sensitive -
-
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss index fdc3dc033..0d4e59c2a 100644 --- a/client/src/app/shared/video/video-miniature.component.scss +++ b/client/src/app/shared/video/video-miniature.component.scss @@ -56,6 +56,37 @@ } } + .video-bottom { + display: flex; + + .video-actions { + margin-top: 3px; + margin-right: 10px; + } + + /deep/ .dropdown-root:not(.show) { + display: none; + } + + &:hover /deep/ .dropdown-root { + display: block; + } + + /deep/ .playlist-dropdown.show + my-action-dropdown .dropdown-root { + display: block; + } + + @media screen and (max-width: $small-view) { + .video-actions { + margin-right: 0; + } + + /deep/ .dropdown-root { + display: block !important; + } + } + } + &.display-as-row { flex-direction: row; margin-bottom: 0; @@ -91,6 +122,11 @@ } } + .video-bottom .video-actions { + margin: 0; + top: -3px; + } + @media screen and (max-width: $small-view) { flex-direction: column; height: auto; diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts index 800417a79..e3552abba 100644 --- a/client/src/app/shared/video/video-miniature.component.ts +++ b/client/src/app/shared/video/video-miniature.component.ts @@ -1,9 +1,11 @@ -import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core' +import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, LOCALE_ID, OnInit, Output } from '@angular/core' import { User } from '../users' import { Video } from './video.model' import { ServerService } from '@app/core' import { VideoPrivacy, VideoState } from '../../../../../shared' import { I18n } from '@ngx-translate/i18n-polyfill' +import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component' +import { ScreenService } from '@app/shared/misc/screen.service' export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' export type MiniatureDisplayOptions = { @@ -38,10 +40,26 @@ export class VideoMiniatureComponent implements OnInit { blacklistInfo: false } @Input() displayAsRow = false + @Input() displayVideoActions = true + + @Output() videoBlacklisted = new EventEmitter() + @Output() videoUnblacklisted = new EventEmitter() + @Output() videoRemoved = new EventEmitter() + + videoActionsDisplayOptions: VideoActionsDisplayType = { + playlist: true, + download: false, + update: true, + blacklist: true, + delete: true, + report: true + } + showActions = false private ownerDisplayTypeChosen: 'account' | 'videoChannel' constructor ( + private screenService: ScreenService, private serverService: ServerService, private i18n: I18n, @Inject(LOCALE_ID) private localeId: string @@ -52,20 +70,10 @@ export class VideoMiniatureComponent implements OnInit { } ngOnInit () { - if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { - this.ownerDisplayTypeChosen = this.ownerDisplayType - return - } + this.setUpBy() - // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) - // -> Use the account name - if ( - this.video.channel.name === `${this.video.account.name}_channel` || - this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) - ) { - this.ownerDisplayTypeChosen = 'account' - } else { - this.ownerDisplayTypeChosen = 'videoChannel' + if (this.screenService.isInSmallView()) { + this.showActions = true } } @@ -109,4 +117,38 @@ export class VideoMiniatureComponent implements OnInit { return '' } + + loadActions () { + if (this.displayVideoActions) this.showActions = true + } + + onVideoBlacklisted () { + this.videoBlacklisted.emit() + } + + onVideoUnblacklisted () { + this.videoUnblacklisted.emit() + } + + onVideoRemoved () { + this.videoRemoved.emit() + } + + private setUpBy () { + if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { + this.ownerDisplayTypeChosen = this.ownerDisplayType + return + } + + // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) + // -> Use the account name + if ( + this.video.channel.name === `${this.video.account.name}_channel` || + this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) + ) { + this.ownerDisplayTypeChosen = 'account' + } else { + this.ownerDisplayTypeChosen = 'videoChannel' + } + } } diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 95b5e3671..0cef3eb8f 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts @@ -1,11 +1,12 @@ import { User } from '../' -import { PlaylistElement, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' +import { PlaylistElement, UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' import { Avatar } from '../../../../../shared/models/avatars/avatar.model' import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' import { durationToString, getAbsoluteAPIUrl } from '../misc/utils' import { peertubeTranslate, ServerConfig } from '../../../../../shared/models' import { Actor } from '@app/shared/actor/actor.model' import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' +import { AuthUser } from '@app/core' export class Video implements VideoServerModel { byVideoChannel: string @@ -141,4 +142,20 @@ export class Video implements VideoServerModel { // Return default instance config return serverConfig.instance.defaultNSFWPolicy !== 'display' } + + isRemovableBy (user: AuthUser) { + return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) + } + + isBlackistableBy (user: AuthUser) { + return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true + } + + isUnblacklistableBy (user: AuthUser) { + return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true + } + + isUpdatableBy (user: AuthUser) { + return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) + } } diff --git a/client/src/app/shared/video/videos-selection.component.html b/client/src/app/shared/video/videos-selection.component.html index 6f3401b4b..53809b6fd 100644 --- a/client/src/app/shared/video/videos-selection.component.html +++ b/client/src/app/shared/video/videos-selection.component.html @@ -6,7 +6,7 @@
- +
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 ad1d04b70..7755a729a 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html @@ -120,37 +120,9 @@
- +
- - - 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 2874847cd..c1eaf9b2b 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss @@ -257,7 +257,9 @@ $player-factor: 1.7; // 16/9 display: flex; align-items: center; - .action-button:not(:first-child), .action-dropdown { + .action-button:not(:first-child), + .action-dropdown, + my-video-actions-dropdown { margin-left: 10px; } @@ -304,14 +306,6 @@ $player-factor: 1.7; // 16/9 margin-left: 3px; } } - - .action-dropdown { - display: inline-block; - - .dropdown-menu .dropdown-item { - @include dropdown-with-icon-item; - } - } } .video-info-likes-dislikes-bar { 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 cedbbf985..53673d9d9 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -13,10 +13,7 @@ import { AuthService, ConfirmService } from '../../core' import { RestExtractor, VideoBlacklistService } from '../../shared' import { VideoDetails } from '../../shared/video/video-details.model' import { VideoService } from '../../shared/video/video.service' -import { VideoDownloadComponent } from './modal/video-download.component' -import { VideoReportComponent } from './modal/video-report.component' import { VideoShareComponent } from './modal/video-share.component' -import { VideoBlacklistComponent } from './modal/video-blacklist.component' import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' import { I18n } from '@ngx-translate/i18n-polyfill' import { environment } from '../../../environments/environment' @@ -32,6 +29,7 @@ import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' import { Video } from '@app/shared/video/video.model' +import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component' @Component({ selector: 'my-video-watch', @@ -41,11 +39,8 @@ import { Video } from '@app/shared/video/video.model' export class VideoWatchComponent implements OnInit, OnDestroy { private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern' - @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent @ViewChild('videoShareModal') videoShareModal: VideoShareComponent - @ViewChild('videoReportModal') videoReportModal: VideoReportComponent @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent - @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent player: any @@ -212,11 +207,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { ) } - showReportModal (event: Event) { - event.preventDefault() - this.videoReportModal.show() - } - showSupportModal () { this.videoSupportModal.show() } @@ -225,54 +215,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.videoShareModal.show(this.currentTime) } - showDownloadModal (event: Event) { - event.preventDefault() - this.videoDownloadModal.show() - } - - showBlacklistModal (event: Event) { - event.preventDefault() - this.videoBlacklistModal.show() - } - - async unblacklistVideo (event: Event) { - event.preventDefault() - - const confirmMessage = this.i18n( - 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.' - ) - - const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist')) - if (res === false) return - - this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe( - () => { - this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name })) - - this.video.blacklisted = false - this.video.blacklistedReason = null - }, - - err => this.notifier.error(err.message) - ) - } - isUserLoggedIn () { return this.authService.isLoggedIn() } - isVideoUpdatable () { - return this.video.isUpdatableBy(this.authService.getUser()) - } - - isVideoBlacklistable () { - return this.video.isBlackistableBy(this.user) - } - - isVideoUnblacklistable () { - return this.video.isUnblacklistableBy(this.user) - } - getVideoTags () { if (!this.video || Array.isArray(this.video.tags) === false) return [] @@ -283,23 +229,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { return this.video.isRemovableBy(this.authService.getUser()) } - async removeVideo (event: Event) { - event.preventDefault() - - const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete')) - if (res === false) return - - this.videoService.removeVideo(this.video.id) - .subscribe( - () => { - this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })) - - // Go back to the video-list. - this.redirectService.redirectToHomepage() - }, - - error => this.notifier.error(error.message) - ) + onVideoRemoved () { + this.redirectService.redirectToHomepage() } acceptedPrivacyConcern () { diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts index 2f448db78..983350f52 100644 --- a/client/src/app/videos/+video-watch/video-watch.module.ts +++ b/client/src/app/videos/+video-watch/video-watch.module.ts @@ -1,26 +1,21 @@ import { NgModule } from '@angular/core' import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' -import { ClipboardModule } from 'ngx-clipboard' import { SharedModule } from '../../shared' import { VideoCommentAddComponent } from './comment/video-comment-add.component' import { VideoCommentComponent } from './comment/video-comment.component' import { VideoCommentService } from './comment/video-comment.service' import { VideoCommentsComponent } from './comment/video-comments.component' -import { VideoDownloadComponent } from './modal/video-download.component' -import { VideoReportComponent } from './modal/video-report.component' import { VideoShareComponent } from './modal/video-share.component' import { VideoWatchRoutingModule } from './video-watch-routing.module' import { VideoWatchComponent } from './video-watch.component' import { NgxQRCodeModule } from 'ngx-qrcode2' import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' -import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component' import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' @NgModule({ imports: [ VideoWatchRoutingModule, SharedModule, - ClipboardModule, NgbTooltipModule, NgxQRCodeModule, RecommendationsModule @@ -29,10 +24,7 @@ import { RecommendationsModule } from '@app/videos/recommendations/recommendatio declarations: [ VideoWatchComponent, - VideoDownloadComponent, VideoShareComponent, - VideoReportComponent, - VideoBlacklistComponent, VideoSupportComponent, VideoCommentsComponent, VideoCommentAddComponent, diff --git a/client/src/app/videos/video-list/video-overview.component.html b/client/src/app/videos/video-list/video-overview.component.html index cb26592e3..b644dd798 100644 --- a/client/src/app/videos/video-list/video-overview.component.html +++ b/client/src/app/videos/video-list/video-overview.component.html @@ -7,7 +7,7 @@ {{ object.category.label }}
- +
@@ -15,7 +15,7 @@ #{{ object.tag }}
- +
@@ -27,7 +27,7 @@
- +