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 4b94490a09196571f8ad49d53fbc412b2cf3a8fe..6e274f6898d8de13de6b8a086424d794cb9fee7f 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 @@ <div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos"> <div class="video" *ngFor="let video of videos"> - <my-video-miniature [video]="video" [displayAsRow]="true"></my-video-miniature> + <my-video-miniature + [video]="video" [displayAsRow]="true" + (videoRemoved)="removeVideoFromArray(video)" (videoBlacklisted)="removeVideoFromArray(video)"></my-video-miniature> </div> </div> diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html index da2ace54de3790ed8d8de52e4ec4a171416f5ba5..0a9f78cb26c55ed7fa4e913d7ff05bf9e8455192 100644 --- a/client/src/app/search/search.component.html +++ b/client/src/app/search/search.component.html @@ -48,7 +48,10 @@ </div> <div *ngIf="isVideo(result)" class="entry video"> - <my-video-miniature [video]="result" [user]="user" [displayAsRow]="true"></my-video-miniature> + <my-video-miniature + [video]="result" [user]="user" [displayAsRow]="true" + (videoBlacklisted)="removeVideoFromArray(result)" (videoRemoved)="removeVideoFromArray(result)" + ></my-video-miniature> </div> </ng-container> diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts index a3383ed8a08f138c92e74f14b650f4fb3bcc79ec..a7ddbe1f8fb78437c3b8fd98fd67aaf9e08324e0 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 6999474d64bccb0bdaaeb86f0229cfc0395305c0..cc244dc760ad5f710b6ddf7ec9b25cb5170c54df 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 @@ <div class="dropdown-root" ngbDropdown [placement]="placement"> <div - class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }" + class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange', 'button-styled': buttonStyled }" ngbDropdownToggle role="button" > - <my-global-icon *ngIf="!label" class="more-icon" iconName="more-horizontal"></my-global-icon> + <my-global-icon *ngIf="!label && buttonDirection === 'horizontal'" class="more-icon" iconName="more-horizontal"></my-global-icon> + <my-global-icon *ngIf="!label && buttonDirection === 'vertical'" class="more-icon" iconName="more-vertical"></my-global-icon> + <span *ngIf="label" class="dropdown-toggle">{{ label }}</span> </div> @@ -12,15 +14,24 @@ <ng-container *ngFor="let action of actions"> <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> - <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a> - <span *ngIf="!action.linkBuilder" class="custom-action dropdown-item" (click)="action.handler(entry)" role="button"> + <a *ngIf="action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" class="dropdown-item" [routerLink]="action.linkBuilder(entry)"> + <my-global-icon *ngIf="action.iconName" [iconName]="action.iconName" [ngClass]="'icon-' + action.iconName"></my-global-icon> + {{ action.label }} + </a> + + <span + *ngIf="!action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" (click)="action.handler(entry)" + class="custom-action dropdown-item" role="button" + > + <my-global-icon *ngIf="action.iconName" [iconName]="action.iconName" [ngClass]="'icon-' + action.iconName"></my-global-icon> {{ action.label }} </span> + </ng-container> </ng-container> - <div class="dropdown-divider"></div> + <div *ngIf="areActionsDisplayed(actions, entry)" class="dropdown-divider"></div> </ng-container> </div> diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss index 985b2ca8844da898537e0e4a69b544295c71abbe..5073190b0bd36c1c12360879916728eb458305d2 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 { + + &.grey { + @include grey-button; + } + + &.orange { + @include orange-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 275e2b51ecfbabd82e4816b18c3414f8e9a60dd7..f5345831b86dd5827d28a11a14411347945ecdc1 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<T> = { 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<T> = { export class ActionDropdownComponent<T> { @Input() actions: DropdownAction<T>[] | DropdownAction<T>[][] = [] @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<T>[], entry: T) { + return actions.some(a => a.isDisplayed === undefined || a.isDisplayed(entry)) + } + + handleClick (event: Event, action: DropdownAction<T>) { + 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 1cbc96b14b857468746111fb7c96f5717f91ed4f..db481204e5ced7d75e971ae7821c9c62bd8f0ade 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 68225b457fbf6351d58fbd7ef1deb3b2db531b82..ded65653f224ee1e93c7cbd1c289dbe62b49d6c7 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 19b326206a52bb1b2efb0e7392596cfa7b7cf2b6..6029b364850208b361638e64f7d4481d54ae2969 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 @@ -<div class="header"> - <div class="first-row"> - <div i18n class="title">Save to</div> +<div class="root"> + <div class="header"> + <div class="first-row"> + <div i18n class="title">Save to</div> - <div class="options" (click)="displayOptions = !displayOptions"> - <my-global-icon iconName="cog"></my-global-icon> + <div class="options" (click)="displayOptions = !displayOptions"> + <my-global-icon iconName="cog"></my-global-icon> - <span i18n>Options</span> + <span i18n>Options</span> + </div> </div> - </div> - <div class="options-row" *ngIf="displayOptions"> - <div> - <my-peertube-checkbox - inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled" - i18n-labelText labelText="Start at" - ></my-peertube-checkbox> + <div class="options-row" *ngIf="displayOptions"> + <div> + <my-peertube-checkbox + inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled" + i18n-labelText labelText="Start at" + ></my-peertube-checkbox> - <my-timestamp-input - [timestamp]="timestampOptions.startTimestamp" - [maxTimestamp]="video.duration" - [disabled]="!timestampOptions.startTimestampEnabled" - [(ngModel)]="timestampOptions.startTimestamp" - ></my-timestamp-input> - </div> + <my-timestamp-input + [timestamp]="timestampOptions.startTimestamp" + [maxTimestamp]="video.duration" + [disabled]="!timestampOptions.startTimestampEnabled" + [(ngModel)]="timestampOptions.startTimestamp" + ></my-timestamp-input> + </div> - <div> - <my-peertube-checkbox - inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" - i18n-labelText labelText="Stop at" - ></my-peertube-checkbox> + <div> + <my-peertube-checkbox + inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" + i18n-labelText labelText="Stop at" + ></my-peertube-checkbox> - <my-timestamp-input - [timestamp]="timestampOptions.stopTimestamp" - [maxTimestamp]="video.duration" - [disabled]="!timestampOptions.stopTimestampEnabled" - [(ngModel)]="timestampOptions.stopTimestamp" - ></my-timestamp-input> + <my-timestamp-input + [timestamp]="timestampOptions.stopTimestamp" + [maxTimestamp]="video.duration" + [disabled]="!timestampOptions.stopTimestampEnabled" + [(ngModel)]="timestampOptions.stopTimestamp" + ></my-timestamp-input> + </div> </div> </div> -</div> -<div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)"> - <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist"></my-peertube-checkbox> + <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)"> + <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist"></my-peertube-checkbox> - <div class="display-name"> - {{ playlist.displayName }} + <div class="display-name"> + {{ playlist.displayName }} - <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info"> - {{ formatTimestamp(playlist) }} + <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info"> + {{ formatTimestamp(playlist) }} + </div> </div> </div> -</div> -<div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened"> - <my-global-icon iconName="add"></my-global-icon> + <div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened"> + <my-global-icon iconName="add"></my-global-icon> - Create a new playlist -</div> + Create a new playlist + </div> -<form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form"> - <div class="form-group"> - <label i18n for="displayName">Display name</label> - <input - type="text" id="displayName" - formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" - > - <div *ngIf="formErrors['displayName']" class="form-error"> - {{ formErrors['displayName'] }} + <form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form"> + <div class="form-group"> + <label i18n for="displayName">Display name</label> + <input + type="text" id="displayName" + formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" + > + <div *ngIf="formErrors['displayName']" class="form-error"> + {{ formErrors['displayName'] }} + </div> </div> - </div> - <input type="submit" i18n-value value="Create" [disabled]="!form.valid"> -</form> + <input type="submit" i18n-value value="Create" [disabled]="!form.valid"> + </form> +</div> 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 bc0d55912bcdea8547197cba3f5fa97adbf485bf..0424e2ee9814edcf88b3627e07963d4910be518a 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 705f62404daf3c2586d8d582d6c6077d8b6f1540..152f20c856014e3754ce333020daded6ef84d5bb 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 e134654a35a63ad88863cec4171b6885050f0920..d1b761674a4ba2cded6086bc891eb031db0211f0 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 @@ -<div [ngClass]="{ 'margin-content': marginContent }"> +<div class="margin-content"> <div class="videos-header"> <div *ngIf="titlePage" class="title-page title-page-single"> <div placement="bottom" [ngbTooltip]="titleTooltip" container="body"> @@ -11,7 +11,7 @@ <div class="moderation-block" *ngIf="displayModerationBlock"> <my-peertube-checkbox (change)="toggleModerationDisplay()" - inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos" + inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos" > </my-peertube-checkbox> </div> @@ -22,7 +22,11 @@ myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos" > - <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"> + <my-video-miniature + *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType" + [displayVideoActions]="displayVideoActions" + (videoBlacklisted)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" + > </my-video-miniature> </div> </div> diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 09965012999bdf5e0a4d485c6e83eea41d922a0d..cf43d429d04790d85ed09a930550ed82aa0e5e92 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 50a7cadd1acc1cc020d04486e6b0257a90070e69..4e4e8dc50324ff8e8c1a8a021db8d0c351a405c7 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 8343857717d4301a23ce2800f3c4f694ec1a1144..64aaeb3c86ffa26e4d3c435ac17e2a38625ca7b3 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 + + case 'torrent': + return file.torrentDownloadUrl - return link + 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 911f3b4479e888b0757e324bcf17b7376fa9af51..725dd020f21fbf3801cf3c9a02fedfe13ccbfa18 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 0000000000000000000000000000000000000000..300fe318aa1f2debd4627785a469a873ddbd162e --- /dev/null +++ b/client/src/app/shared/video/video-actions-dropdown.component.html @@ -0,0 +1,21 @@ +<ng-container *ngIf="videoActions.length !== 0"> + + <div class="playlist-dropdown" ngbDropdown #playlistDropdown="ngbDropdown" role="button" autoClose="outside" [placement]="getPlaylistDropdownPlacement()" + *ngIf="isUserLoggedIn() && displayOptions.playlist" (openChange)="playlistAdd.openChange($event)" + > + <span class="anchor" ngbDropdownAnchor></span> + + <div ngbDropdownMenu> + <my-video-add-to-playlist #playlistAdd [video]="video" [lazyLoad]="true"></my-video-add-to-playlist> + </div> + </div> + + <my-action-dropdown + [actions]="videoActions" [label]="label" [entry]="{ video: video }" (mouseenter)="loadDropdownInformation()" + [buttonSize]="buttonSize" [placement]="placement" [buttonDirection]="buttonDirection" [buttonStyled]="buttonStyled" + ></my-action-dropdown> + + <my-video-download #videoDownloadModal></my-video-download> + <my-video-report #videoReportModal [video]="video"></my-video-report> + <my-video-blacklist #videoBlacklistModal [video]="video" (videoBlacklisted)="onVideoBlacklisted()"></my-video-blacklist> +</ng-container> 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 0000000000000000000000000000000000000000..7ffdce822302ddee2eea24a7a1f2c7855eea5ac4 --- /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 0000000000000000000000000000000000000000..90bdf7df8f0b271aa97eee4ca507acbe577bbbfb --- /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 38835734363a6c5040d002a45fb1ee4f6df976ed..8463e15d750491e3f5afe997acceb0c29232d5da 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 f4ae0b0dd5d9d44c2a943f515dcf90c183729314..7af0f111380eeccb670baa5db0e507c6836783db 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 @@ -<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow }"> +<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow }" (mouseenter)="loadActions()"> <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur"></my-video-thumbnail> - <div class="video-miniature-information"> - <a - tabindex="-1" - class="video-miniature-name" - [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" - > - <ng-container *ngIf="displayOptions.privacyLabel"> - <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span> - <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span> - </ng-container> - - {{ video.name }} - </a> - - <span class="video-miniature-created-at-views"> - <ng-container *ngIf="displayOptions.date">{{ video.publishedAt | myFromNow }}</ng-container> - <ng-container *ngIf="displayOptions.date && displayOptions.views"> - </ng-container> - <ng-container i18n *ngIf="displayOptions.views">{{ video.views | myNumberFormatter }} views</ng-container> - </span> - - <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> - {{ video.byAccount }} - </a> - <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> - {{ video.byVideoChannel }} - </a> - - <div class="video-info-privacy"> - <ng-container *ngIf="displayOptions.privacyText">{{ video.privacy.label }}</ng-container> - <ng-container *ngIf="displayOptions.privacyText && displayOptions.state"> - </ng-container> - <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> + <div class="video-bottom"> + <div class="video-miniature-information"> + <a + tabindex="-1" + class="video-miniature-name" + [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" + > + <ng-container *ngIf="displayOptions.privacyLabel"> + <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span> + <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span> + </ng-container> + + {{ video.name }} + </a> + + <span class="video-miniature-created-at-views"> + <ng-container *ngIf="displayOptions.date">{{ video.publishedAt | myFromNow }}</ng-container> + <ng-container *ngIf="displayOptions.date && displayOptions.views"> - </ng-container> + <ng-container i18n *ngIf="displayOptions.views">{{ video.views | myNumberFormatter }} views</ng-container> + </span> + + <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> + {{ video.byAccount }} + </a> + <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> + {{ video.byVideoChannel }} + </a> + + <div class="video-info-privacy"> + <ng-container *ngIf="displayOptions.privacyText">{{ video.privacy.label }}</ng-container> + <ng-container *ngIf="displayOptions.privacyText && displayOptions.state"> - </ng-container> + <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> + </div> + + <div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blacklisted"> + <span class="blacklisted-label" i18n>Blacklisted</span> + <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span> + </div> + + <div i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw"> + Sensitive + </div> </div> - <div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blacklisted"> - <span class="blacklisted-label" i18n>Blacklisted</span> - <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span> + <div class="video-actions"> + <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown --> + <my-video-actions-dropdown + *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left" + (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()" + ></my-video-actions-dropdown> </div> - - <div i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw"> - Sensitive - </div> - </div> </div> diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss index fdc3dc0339e6940220bbff6b9476d27bee84a865..0d4e59c2a372552de8419cf8af6a41ec33b14e92 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 800417a7994174a55e81082aafcb47a12d8a270f..e3552abbaf4caa8297214b575e90f3ed62c2a1d1 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 95b5e36713b35f3211178fe2895622c5639a1d4c..0cef3eb8f1de5f17c2992c9a6134ae0806ae366b 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 6f3401b4b9f6bc8a8bcdde1d6be47c4dec9093cb..53809b6fdb198e35d192ec8eec33fcf8ceadd90a 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 @@ <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="_selection[video.id]"></my-peertube-checkbox> </div> - <my-video-miniature [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions"></my-video-miniature> + <my-video-miniature [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" [displayVideoActions]="false"></my-video-miniature> <!-- Display only once --> <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0"> 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 ad1d04b702a7160bbae7fd60b75bdfcf290556af..7755a729a176c5b663c10276aeacd05c7bf38155 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 @@ </div> </div> - <div class="action-dropdown" ngbDropdown placement="top" role="button"> - <div class="action-button" ngbDropdownToggle role="button"> - <my-global-icon class="more-icon" iconName="more-horizontal"></my-global-icon> - </div> - - <div ngbDropdownMenu> - <a *ngIf="isVideoDownloadable()" class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> - <my-global-icon iconName="download"></my-global-icon> <ng-container i18n>Download</ng-container> - </a> - - <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> - <my-global-icon iconName="alert"></my-global-icon> <ng-container i18n>Report</ng-container> - </a> - - <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> - <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Update</ng-container> - </a> - - <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> - <my-global-icon iconName="no"></my-global-icon> <ng-container i18n>Blacklist</ng-container> - </a> - - <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> - <my-global-icon iconName="undo"></my-global-icon> <ng-container i18n>Unblacklist</ng-container> - </a> - - <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> - <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete</ng-container> - </a> - </div> - </div> + <my-video-actions-dropdown + placement="top" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" (videoRemoved)="onVideoRemoved()" + ></my-video-actions-dropdown> </div> <div @@ -270,7 +242,4 @@ <ng-template [ngIf]="video !== null"> <my-video-support #videoSupportModal [video]="video"></my-video-support> <my-video-share #videoShareModal [video]="video"></my-video-share> - <my-video-download #videoDownloadModal [video]="video"></my-video-download> - <my-video-report #videoReportModal [video]="video"></my-video-report> - <my-video-blacklist #videoBlacklistModal [video]="video"></my-video-blacklist> </ng-template> 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 2874847cd31c2d6f26487197eae03bd00a69941a..c1eaf9b2b0d192917ae880bf48bd15a06abe941f 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 cedbbf985536ae1abcc6d256bb2d937eb38e90bd..53673d9d92a92c761a57c35360131469c2ed5086 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 2f448db780c9000f899187d28cb39577b1590914..983350f52c2c7bf73a39f0b094876d584981ad22 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 cb26592e3b71223edb7d363bc103aefead224f93..b644dd7982fdb7e3f793c2513d05d2bb9df5e048 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 @@ <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> </div> - <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user"></my-video-miniature> + <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature> </div> <div class="section" *ngFor="let object of overview.tags"> @@ -15,7 +15,7 @@ <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> </div> - <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user"></my-video-miniature> + <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature> </div> <div class="section channel" *ngFor="let object of overview.channels"> @@ -27,7 +27,7 @@ </a> </div> - <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user"></my-video-miniature> + <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature> </div> </div>