1
0
Fork 0

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 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos">
<div class="video" *ngFor="let video of 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>
</div> </div>

View File

@ -48,7 +48,10 @@
</div> </div>
<div *ngIf="isVideo(result)" class="entry video"> <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> </div>
</ng-container> </ng-container>

View File

@ -1,6 +1,6 @@
import { Component, OnDestroy, OnInit } from '@angular/core' import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' 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 { forkJoin, Subscription } from 'rxjs'
import { SearchService } from '@app/search/search.service' import { SearchService } from '@app/search/search.service'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model' import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
@ -138,6 +138,10 @@ export class SearchComponent implements OnInit, OnDestroy {
return this.advancedSearch.size() return this.advancedSearch.size()
} }
removeVideoFromArray (video: Video) {
this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id)
}
private resetPagination () { private resetPagination () {
this.pagination.currentPage = 1 this.pagination.currentPage = 1
this.pagination.totalItems = null this.pagination.totalItems = null

View File

@ -1,9 +1,11 @@
<div class="dropdown-root" ngbDropdown [placement]="placement"> <div class="dropdown-root" ngbDropdown [placement]="placement">
<div <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" 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> <span *ngIf="label" class="dropdown-toggle">{{ label }}</span>
</div> </div>
@ -12,15 +14,24 @@
<ng-container *ngFor="let action of actions"> <ng-container *ngFor="let action of actions">
<ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> <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 }} {{ action.label }}
</span> </span>
</ng-container> </ng-container>
</ng-container> </ng-container>
<div class="dropdown-divider"></div> <div *ngIf="areActionsDisplayed(actions, entry)" class="dropdown-divider"></div>
</ng-container> </ng-container>
</div> </div>

View File

@ -8,12 +8,19 @@
.action-button { .action-button {
@include peertube-button; @include peertube-button;
&.grey { &.button-styled {
@include grey-button;
}
&.orange { &.grey {
@include orange-button; @include grey-button;
}
&.orange {
@include orange-button;
}
&:hover, &:active, &:focus {
background-color: $grey-background-color;
}
} }
display: inline-block; display: inline-block;
@ -23,10 +30,6 @@
display: none; display: none;
} }
&:hover, &:active, &:focus {
background-color: $grey-background-color;
}
.more-icon { .more-icon {
width: 21px; width: 21px;
} }
@ -48,6 +51,10 @@
cursor: pointer; cursor: pointer;
color: #000 !important; color: #000 !important;
&.with-icon {
@include dropdown-with-icon-item;
}
a, span { a, span {
display: block; display: block;
width: 100%; width: 100%;

View File

@ -1,12 +1,18 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { GlobalIconName } from '@app/shared/images/global-icon.component'
export type DropdownAction<T> = { export type DropdownAction<T> = {
label?: string label?: string
iconName?: GlobalIconName
handler?: (a: T) => any handler?: (a: T) => any
linkBuilder?: (a: T) => (string | number)[] linkBuilder?: (a: T) => (string | number)[]
isDisplayed?: (a: T) => boolean isDisplayed?: (a: T) => boolean
} }
export type DropdownButtonSize = 'normal' | 'small'
export type DropdownTheme = 'orange' | 'grey'
export type DropdownDirection = 'horizontal' | 'vertical'
@Component({ @Component({
selector: 'my-action-dropdown', selector: 'my-action-dropdown',
styleUrls: [ './action-dropdown.component.scss' ], styleUrls: [ './action-dropdown.component.scss' ],
@ -16,14 +22,29 @@ export type DropdownAction<T> = {
export class ActionDropdownComponent<T> { export class ActionDropdownComponent<T> {
@Input() actions: DropdownAction<T>[] | DropdownAction<T>[][] = [] @Input() actions: DropdownAction<T>[] | DropdownAction<T>[][] = []
@Input() entry: T @Input() entry: T
@Input() placement = 'bottom-left' @Input() placement = 'bottom-left'
@Input() buttonSize: 'normal' | 'small' = 'normal'
@Input() buttonSize: DropdownButtonSize = 'normal'
@Input() buttonDirection: DropdownDirection = 'horizontal'
@Input() buttonStyled = true
@Input() label: string @Input() label: string
@Input() theme: 'orange' | 'grey' = 'grey' @Input() theme: DropdownTheme = 'grey'
getActions () { getActions () {
if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions
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 () { private cacheWindowInnerWidthExpired () {
if (!this.lastFunctionCallTime) return true
return new Date().getTime() > (this.lastFunctionCallTime + this.cacheForMs) 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 { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
import { FromNowPipe } from '@app/shared/angular/from-now.pipe' import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' 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({ @NgModule({
imports: [ imports: [
@ -95,6 +100,8 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
NgbTabsetModule, NgbTabsetModule,
NgbTooltipModule, NgbTooltipModule,
ClipboardModule,
PrimeSharedModule, PrimeSharedModule,
InputMaskModule, InputMaskModule,
NgPipesModule NgPipesModule
@ -110,6 +117,11 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
VideoAddToPlaylistComponent, VideoAddToPlaylistComponent,
VideoPlaylistElementMiniatureComponent, VideoPlaylistElementMiniatureComponent,
VideosSelectionComponent, VideosSelectionComponent,
VideoActionsDropdownComponent,
VideoDownloadComponent,
VideoReportComponent,
VideoBlacklistComponent,
FeedComponent, FeedComponent,
@ -158,6 +170,8 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
NgbTabsetModule, NgbTabsetModule,
NgbTooltipModule, NgbTooltipModule,
ClipboardModule,
PrimeSharedModule, PrimeSharedModule,
InputMaskModule, InputMaskModule,
BytesPipe, BytesPipe,
@ -172,6 +186,11 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
VideoAddToPlaylistComponent, VideoAddToPlaylistComponent,
VideoPlaylistElementMiniatureComponent, VideoPlaylistElementMiniatureComponent,
VideosSelectionComponent, VideosSelectionComponent,
VideoActionsDropdownComponent,
VideoDownloadComponent,
VideoReportComponent,
VideoBlacklistComponent,
FeedComponent, FeedComponent,

View File

@ -1,74 +1,76 @@
<div class="header"> <div class="root">
<div class="first-row"> <div class="header">
<div i18n class="title">Save to</div> <div class="first-row">
<div i18n class="title">Save to</div>
<div class="options" (click)="displayOptions = !displayOptions"> <div class="options" (click)="displayOptions = !displayOptions">
<my-global-icon iconName="cog"></my-global-icon> <my-global-icon iconName="cog"></my-global-icon>
<span i18n>Options</span> <span i18n>Options</span>
</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>
<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>
<my-timestamp-input
[timestamp]="timestampOptions.stopTimestamp"
[maxTimestamp]="video.duration"
[disabled]="!timestampOptions.stopTimestampEnabled"
[(ngModel)]="timestampOptions.stopTimestamp"
></my-timestamp-input>
</div>
</div> </div>
</div> </div>
<div class="options-row" *ngIf="displayOptions"> <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
<div> <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist"></my-peertube-checkbox>
<my-peertube-checkbox
inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
i18n-labelText labelText="Start at"
></my-peertube-checkbox>
<my-timestamp-input <div class="display-name">
[timestamp]="timestampOptions.startTimestamp" {{ playlist.displayName }}
[maxTimestamp]="video.duration"
[disabled]="!timestampOptions.startTimestampEnabled"
[(ngModel)]="timestampOptions.startTimestamp"
></my-timestamp-input>
</div>
<div> <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info">
<my-peertube-checkbox {{ formatTimestamp(playlist) }}
inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" </div>
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>
</div> </div>
</div> </div>
<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>
<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>
<input type="submit" i18n-value value="Create" [disabled]="!form.valid">
</form>
</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="display-name">
{{ playlist.displayName }}
<div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info">
{{ formatTimestamp(playlist) }}
</div>
</div>
</div>
<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>
<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>
<input type="submit" i18n-value value="Create" [disabled]="!form.valid">
</form>

View File

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

View File

@ -24,6 +24,7 @@ type PlaylistSummary = {
export class VideoAddToPlaylistComponent extends FormReactive implements OnInit { export class VideoAddToPlaylistComponent extends FormReactive implements OnInit {
@Input() video: Video @Input() video: Video
@Input() currentVideoTimestamp: number @Input() currentVideoTimestamp: number
@Input() lazyLoad = false
isNewPlaylistBlockOpened = false isNewPlaylistBlockOpened = false
videoPlaylists: PlaylistSummary[] = [] videoPlaylists: PlaylistSummary[] = []
@ -57,6 +58,10 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit
displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME
}) })
if (this.lazyLoad !== true) this.load()
}
load () {
forkJoin([ forkJoin([
this.videoPlaylistService.listAccountPlaylists(this.user.account, '-updatedAt'), this.videoPlaylistService.listAccountPlaylists(this.user.account, '-updatedAt'),
this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id) 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 class="videos-header">
<div *ngIf="titlePage" class="title-page title-page-single"> <div *ngIf="titlePage" class="title-page title-page-single">
<div placement="bottom" [ngbTooltip]="titleTooltip" container="body"> <div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
@ -11,7 +11,7 @@
<div class="moderation-block" *ngIf="displayModerationBlock"> <div class="moderation-block" *ngIf="displayModerationBlock">
<my-peertube-checkbox <my-peertube-checkbox
(change)="toggleModerationDisplay()" (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> </my-peertube-checkbox>
</div> </div>
@ -22,7 +22,11 @@
myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"
class="videos" 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> </my-video-miniature>
</div> </div>
</div> </div>

View File

@ -26,11 +26,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
syndicationItems: Syndication[] = [] syndicationItems: Syndication[] = []
loadOnInit = true loadOnInit = true
marginContent = true
videos: Video[] = [] videos: Video[] = []
ownerDisplayType: OwnerDisplayType = 'account' ownerDisplayType: OwnerDisplayType = 'account'
displayModerationBlock = false displayModerationBlock = false
titleTooltip: string titleTooltip: string
displayVideoActions = true
disabled = false disabled = false
@ -120,6 +120,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
throw new Error('toggleModerationDisplay is not implemented') 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 // On videos hook for children that want to do something
protected onMoreVideos () { /* empty */ } 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 { 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 { VideoDetails } from '../../../shared/video/video-details.model'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms'
@Component({ @Component({
selector: 'my-video-blacklist', selector: 'my-video-blacklist',
@ -17,6 +18,8 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
@ViewChild('modal') modal: NgbModal @ViewChild('modal') modal: NgbModal
@Output() videoBlacklisted = new EventEmitter()
error: string = null error: string = null
private openedModal: NgbModalRef private openedModal: NgbModalRef
@ -60,7 +63,11 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
() => { () => {
this.notifier.success(this.i18n('Video blacklisted.')) this.notifier.success(this.i18n('Video blacklisted.'))
this.hide() this.hide()
this.redirectService.redirectToHomepage()
this.video.blacklisted = true
this.video.blacklistedReason = reason
this.videoBlacklisted.emit()
}, },
err => this.notifier.error(err.message) 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 { VideoDetails } from '../../../shared/video/video-details.model'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
@ -9,26 +9,32 @@ import { Notifier } from '@app/core'
templateUrl: './video-download.component.html', templateUrl: './video-download.component.html',
styleUrls: [ './video-download.component.scss' ] styleUrls: [ './video-download.component.scss' ]
}) })
export class VideoDownloadComponent implements OnInit { export class VideoDownloadComponent {
@Input() video: VideoDetails = null
@ViewChild('modal') modal: ElementRef @ViewChild('modal') modal: ElementRef
downloadType: 'direct' | 'torrent' | 'magnet' = 'torrent' downloadType: 'direct' | 'torrent' | 'magnet' = 'torrent'
resolutionId: number | string = -1 resolutionId: number | string = -1
private video: VideoDetails
constructor ( constructor (
private notifier: Notifier, private notifier: Notifier,
private modalService: NgbModal, private modalService: NgbModal,
private i18n: I18n 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 this.resolutionId = this.video.files[0].resolution.id
} }
show () { onClose () {
this.modalService.open(this.modal) this.video = undefined
} }
download () { download () {
@ -45,21 +51,16 @@ export class VideoDownloadComponent implements OnInit {
return return
} }
const link = (() => { switch (this.downloadType) {
switch (this.downloadType) { case 'direct':
case 'direct': { return file.fileDownloadUrl
return file.fileDownloadUrl
}
case 'torrent': {
return file.torrentDownloadUrl
}
case 'magnet': {
return file.magnetUri
}
}
})()
return link case 'torrent':
return file.torrentDownloadUrl
case 'magnet':
return file.magnetUri
}
} }
activateCopiedMessage () { activateCopiedMessage () {

View File

@ -1,12 +1,13 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core' import { Component, Input, OnInit, ViewChild } from '@angular/core'
import { Notifier } from '@app/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 { VideoDetails } from '../../../shared/video/video-details.model'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { VideoAbuseService } from '@app/shared/video-abuse'
@Component({ @Component({
selector: 'my-video-report', 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() 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 () { buildLikeAndDislikePercents () {
this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100

View File

@ -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> <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur"></my-video-thumbnail>
<div class="video-miniature-information"> <div class="video-bottom">
<a <div class="video-miniature-information">
tabindex="-1" <a
class="video-miniature-name" tabindex="-1"
[routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" 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> <ng-container *ngIf="displayOptions.privacyLabel">
<span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span> <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span>
</ng-container> <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span>
</ng-container>
{{ video.name }} {{ video.name }}
</a> </a>
<span class="video-miniature-created-at-views"> <span class="video-miniature-created-at-views">
<ng-container *ngIf="displayOptions.date">{{ video.publishedAt | myFromNow }}</ng-container> <ng-container *ngIf="displayOptions.date">{{ video.publishedAt | myFromNow }}</ng-container>
<ng-container *ngIf="displayOptions.date && displayOptions.views"> - </ng-container> <ng-container *ngIf="displayOptions.date && displayOptions.views"> - </ng-container>
<ng-container i18n *ngIf="displayOptions.views">{{ video.views | myNumberFormatter }} views</ng-container> <ng-container i18n *ngIf="displayOptions.views">{{ video.views | myNumberFormatter }} views</ng-container>
</span> </span>
<a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]">
{{ video.byAccount }} {{ video.byAccount }}
</a> </a>
<a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
{{ video.byVideoChannel }} {{ video.byVideoChannel }}
</a> </a>
<div class="video-info-privacy"> <div class="video-info-privacy">
<ng-container *ngIf="displayOptions.privacyText">{{ video.privacy.label }}</ng-container> <ng-container *ngIf="displayOptions.privacyText">{{ video.privacy.label }}</ng-container>
<ng-container *ngIf="displayOptions.privacyText && displayOptions.state"> - </ng-container> <ng-container *ngIf="displayOptions.privacyText && displayOptions.state"> - </ng-container>
<ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</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>
<div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blacklisted"> <div class="video-actions">
<span class="blacklisted-label" i18n>Blacklisted</span> <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown -->
<span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span> <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 i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw">
Sensitive
</div>
</div> </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 { &.display-as-row {
flex-direction: row; flex-direction: row;
margin-bottom: 0; margin-bottom: 0;
@ -91,6 +122,11 @@
} }
} }
.video-bottom .video-actions {
margin: 0;
top: -3px;
}
@media screen and (max-width: $small-view) { @media screen and (max-width: $small-view) {
flex-direction: column; flex-direction: column;
height: auto; 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 { User } from '../users'
import { Video } from './video.model' import { Video } from './video.model'
import { ServerService } from '@app/core' import { ServerService } from '@app/core'
import { VideoPrivacy, VideoState } from '../../../../../shared' import { VideoPrivacy, VideoState } from '../../../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill' 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 OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
export type MiniatureDisplayOptions = { export type MiniatureDisplayOptions = {
@ -38,10 +40,26 @@ export class VideoMiniatureComponent implements OnInit {
blacklistInfo: false blacklistInfo: false
} }
@Input() displayAsRow = 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' private ownerDisplayTypeChosen: 'account' | 'videoChannel'
constructor ( constructor (
private screenService: ScreenService,
private serverService: ServerService, private serverService: ServerService,
private i18n: I18n, private i18n: I18n,
@Inject(LOCALE_ID) private localeId: string @Inject(LOCALE_ID) private localeId: string
@ -52,20 +70,10 @@ export class VideoMiniatureComponent implements OnInit {
} }
ngOnInit () { ngOnInit () {
if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { this.setUpBy()
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) if (this.screenService.isInSmallView()) {
// -> Use the account name this.showActions = true
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'
} }
} }
@ -109,4 +117,38 @@ export class VideoMiniatureComponent implements OnInit {
return '' 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 { 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 { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model'
import { durationToString, getAbsoluteAPIUrl } from '../misc/utils' import { durationToString, getAbsoluteAPIUrl } from '../misc/utils'
import { peertubeTranslate, ServerConfig } from '../../../../../shared/models' import { peertubeTranslate, ServerConfig } from '../../../../../shared/models'
import { Actor } from '@app/shared/actor/actor.model' import { Actor } from '@app/shared/actor/actor.model'
import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
import { AuthUser } from '@app/core'
export class Video implements VideoServerModel { export class Video implements VideoServerModel {
byVideoChannel: string byVideoChannel: string
@ -141,4 +142,20 @@ export class Video implements VideoServerModel {
// Return default instance config // Return default instance config
return serverConfig.instance.defaultNSFWPolicy !== 'display' 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> <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="_selection[video.id]"></my-peertube-checkbox>
</div> </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 --> <!-- Display only once -->
<div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0"> <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0">

View File

@ -120,37 +120,9 @@
</div> </div>
</div> </div>
<div class="action-dropdown" ngbDropdown placement="top" role="button"> <my-video-actions-dropdown
<div class="action-button" ngbDropdownToggle role="button"> placement="top" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" (videoRemoved)="onVideoRemoved()"
<my-global-icon class="more-icon" iconName="more-horizontal"></my-global-icon> ></my-video-actions-dropdown>
</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>
</div> </div>
<div <div
@ -270,7 +242,4 @@
<ng-template [ngIf]="video !== null"> <ng-template [ngIf]="video !== null">
<my-video-support #videoSupportModal [video]="video"></my-video-support> <my-video-support #videoSupportModal [video]="video"></my-video-support>
<my-video-share #videoShareModal [video]="video"></my-video-share> <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> </ng-template>

View File

@ -257,7 +257,9 @@ $player-factor: 1.7; // 16/9
display: flex; display: flex;
align-items: center; align-items: center;
.action-button:not(:first-child), .action-dropdown { .action-button:not(:first-child),
.action-dropdown,
my-video-actions-dropdown {
margin-left: 10px; margin-left: 10px;
} }
@ -304,14 +306,6 @@ $player-factor: 1.7; // 16/9
margin-left: 3px; margin-left: 3px;
} }
} }
.action-dropdown {
display: inline-block;
.dropdown-menu .dropdown-item {
@include dropdown-with-icon-item;
}
}
} }
.video-info-likes-dislikes-bar { .video-info-likes-dislikes-bar {

View File

@ -13,10 +13,7 @@ import { AuthService, ConfirmService } from '../../core'
import { RestExtractor, VideoBlacklistService } from '../../shared' import { RestExtractor, VideoBlacklistService } from '../../shared'
import { VideoDetails } from '../../shared/video/video-details.model' import { VideoDetails } from '../../shared/video/video-details.model'
import { VideoService } from '../../shared/video/video.service' 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 { VideoShareComponent } from './modal/video-share.component'
import { VideoBlacklistComponent } from './modal/video-blacklist.component'
import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { environment } from '../../../environments/environment' 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 { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model' import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
import { Video } from '@app/shared/video/video.model' import { Video } from '@app/shared/video/video.model'
import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component'
@Component({ @Component({
selector: 'my-video-watch', selector: 'my-video-watch',
@ -41,11 +39,8 @@ import { Video } from '@app/shared/video/video.model'
export class VideoWatchComponent implements OnInit, OnDestroy { export class VideoWatchComponent implements OnInit, OnDestroy {
private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern' private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
@ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
@ViewChild('videoShareModal') videoShareModal: VideoShareComponent @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
@ViewChild('videoReportModal') videoReportModal: VideoReportComponent
@ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
@ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
@ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
player: any player: any
@ -212,11 +207,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
) )
} }
showReportModal (event: Event) {
event.preventDefault()
this.videoReportModal.show()
}
showSupportModal () { showSupportModal () {
this.videoSupportModal.show() this.videoSupportModal.show()
} }
@ -225,54 +215,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoShareModal.show(this.currentTime) 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 () { isUserLoggedIn () {
return this.authService.isLoggedIn() 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 () { getVideoTags () {
if (!this.video || Array.isArray(this.video.tags) === false) return [] 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()) return this.video.isRemovableBy(this.authService.getUser())
} }
async removeVideo (event: Event) { onVideoRemoved () {
event.preventDefault() this.redirectService.redirectToHomepage()
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)
)
} }
acceptedPrivacyConcern () { acceptedPrivacyConcern () {

View File

@ -1,26 +1,21 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
import { ClipboardModule } from 'ngx-clipboard'
import { SharedModule } from '../../shared' import { SharedModule } from '../../shared'
import { VideoCommentAddComponent } from './comment/video-comment-add.component' import { VideoCommentAddComponent } from './comment/video-comment-add.component'
import { VideoCommentComponent } from './comment/video-comment.component' import { VideoCommentComponent } from './comment/video-comment.component'
import { VideoCommentService } from './comment/video-comment.service' import { VideoCommentService } from './comment/video-comment.service'
import { VideoCommentsComponent } from './comment/video-comments.component' 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 { VideoShareComponent } from './modal/video-share.component'
import { VideoWatchRoutingModule } from './video-watch-routing.module' import { VideoWatchRoutingModule } from './video-watch-routing.module'
import { VideoWatchComponent } from './video-watch.component' import { VideoWatchComponent } from './video-watch.component'
import { NgxQRCodeModule } from 'ngx-qrcode2' import { NgxQRCodeModule } from 'ngx-qrcode2'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 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' import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
@NgModule({ @NgModule({
imports: [ imports: [
VideoWatchRoutingModule, VideoWatchRoutingModule,
SharedModule, SharedModule,
ClipboardModule,
NgbTooltipModule, NgbTooltipModule,
NgxQRCodeModule, NgxQRCodeModule,
RecommendationsModule RecommendationsModule
@ -29,10 +24,7 @@ import { RecommendationsModule } from '@app/videos/recommendations/recommendatio
declarations: [ declarations: [
VideoWatchComponent, VideoWatchComponent,
VideoDownloadComponent,
VideoShareComponent, VideoShareComponent,
VideoReportComponent,
VideoBlacklistComponent,
VideoSupportComponent, VideoSupportComponent,
VideoCommentsComponent, VideoCommentsComponent,
VideoCommentAddComponent, VideoCommentAddComponent,

View File

@ -7,7 +7,7 @@
<a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a>
</div> </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>
<div class="section" *ngFor="let object of overview.tags"> <div class="section" *ngFor="let object of overview.tags">
@ -15,7 +15,7 @@
<a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a>
</div> </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>
<div class="section channel" *ngFor="let object of overview.channels"> <div class="section channel" *ngFor="let object of overview.channels">
@ -27,7 +27,7 @@
</a> </a>
</div> </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>
</div> </div>