Add video miniature dropdown

This commit is contained in:
Chocobozzz 2019-04-05 10:52:27 +02:00
parent 693263e936
commit 3a0fb65c61
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
36 changed files with 648 additions and 306 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -8,6 +8,8 @@
.action-button {
@include peertube-button;
&.button-styled {
&.grey {
@include grey-button;
}
@ -16,6 +18,11 @@
@include orange-button;
}
&:hover, &:active, &:focus {
background-color: $grey-background-color;
}
}
display: inline-block;
padding: 0 10px;
@ -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%;

View File

@ -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)
}
}

View File

@ -32,6 +32,8 @@ export class ScreenService {
}
private cacheWindowInnerWidthExpired () {
if (!this.lastFunctionCallTime) return true
return new Date().getTime() > (this.lastFunctionCallTime + this.cacheForMs)
}
}

View File

@ -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,

View File

@ -1,4 +1,5 @@
<div class="header">
<div class="root">
<div class="header">
<div class="first-row">
<div i18n class="title">Save to</div>
@ -38,9 +39,9 @@
></my-timestamp-input>
</div>
</div>
</div>
</div>
<div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
<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">
@ -50,15 +51,15 @@
{{ formatTimestamp(playlist) }}
</div>
</div>
</div>
</div>
<div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened">
<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>
</div>
<form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form">
<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
@ -71,4 +72,5 @@
</div>
<input type="submit" i18n-value value="Create" [disabled]="!form.valid">
</form>
</form>
</div>

View File

@ -1,6 +1,11 @@
@import '_variables';
@import '_mixins';
.root {
max-height: 300px;
overflow-y: auto;
}
.header {
min-width: 240px;
padding: 6px 24px 10px 24px;

View File

@ -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)

View File

@ -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">
@ -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>

View File

@ -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 */ }

View File

@ -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)

View File

@ -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,22 +51,17 @@ export class VideoDownloadComponent implements OnInit {
return
}
const link = (() => {
switch (this.downloadType) {
case 'direct': {
case 'direct':
return file.fileDownloadUrl
}
case 'torrent': {
case 'torrent':
return file.torrentDownloadUrl
}
case 'magnet': {
case 'magnet':
return file.magnetUri
}
}
})()
return link
}
activateCopiedMessage () {
this.notifier.success(this.i18n('Copied'))

View File

@ -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',

View File

@ -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>

View File

@ -0,0 +1,12 @@
.playlist-dropdown {
position: absolute;
.anchor {
display: block;
opacity: 0;
}
}
/deep/ .icon-playlist-add {
left: 2px;
}

View File

@ -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'
}
])
}
}
}

View File

@ -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

View File

@ -1,6 +1,7 @@
<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-bottom">
<div class="video-miniature-information">
<a
tabindex="-1"
@ -42,6 +43,14 @@
<div i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw">
Sensitive
</div>
</div>
<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>
</div>

View File

@ -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;

View File

@ -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'
}
}
}

View File

@ -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))
}
}

View File

@ -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">

View File

@ -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>

View File

@ -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 {

View File

@ -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.
onVideoRemoved () {
this.redirectService.redirectToHomepage()
},
error => this.notifier.error(error.message)
)
}
acceptedPrivacyConcern () {

View File

@ -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,

View File

@ -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>