<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>
</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>
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'
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
<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>
<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>
.action-button {
@include peertube-button;
- &.grey {
- @include grey-button;
- }
+ &.button-styled {
+
+ &.grey {
+ @include grey-button;
+ }
+
+ &.orange {
+ @include orange-button;
+ }
- &.orange {
- @include orange-button;
+ &:hover, &:active, &:focus {
+ background-color: $grey-background-color;
+ }
}
display: inline-block;
display: none;
}
- &:hover, &:active, &:focus {
- background-color: $grey-background-color;
- }
-
.more-icon {
width: 21px;
}
cursor: pointer;
color: #000 !important;
+ &.with-icon {
+ @include dropdown-with-icon-item;
+ }
+
a, span {
display: block;
width: 100%;
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' ],
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)
+ }
}
}
private cacheWindowInnerWidthExpired () {
+ if (!this.lastFunctionCallTime) return true
+
return new Date().getTime() > (this.lastFunctionCallTime + this.cacheForMs)
}
}
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: [
NgbTabsetModule,
NgbTooltipModule,
+ ClipboardModule,
+
PrimeSharedModule,
InputMaskModule,
NgPipesModule
VideoAddToPlaylistComponent,
VideoPlaylistElementMiniatureComponent,
VideosSelectionComponent,
+ VideoActionsDropdownComponent,
+
+ VideoDownloadComponent,
+ VideoReportComponent,
+ VideoBlacklistComponent,
FeedComponent,
NgbTabsetModule,
NgbTooltipModule,
+ ClipboardModule,
+
PrimeSharedModule,
InputMaskModule,
BytesPipe,
VideoAddToPlaylistComponent,
VideoPlaylistElementMiniatureComponent,
VideosSelectionComponent,
+ VideoActionsDropdownComponent,
+
+ VideoDownloadComponent,
+ VideoReportComponent,
+ VideoBlacklistComponent,
FeedComponent,
-<div class="header">
- <div class="first-row">
- <div i18n class="title">Save to</div>
+<div class="root">
+ <div class="header">
+ <div class="first-row">
+ <div i18n class="title">Save to</div>
- <div class="options" (click)="displayOptions = !displayOptions">
- <my-global-icon iconName="cog"></my-global-icon>
+ <div class="options" (click)="displayOptions = !displayOptions">
+ <my-global-icon iconName="cog"></my-global-icon>
- <span i18n>Options</span>
+ <span i18n>Options</span>
+ </div>
</div>
- </div>
- <div class="options-row" *ngIf="displayOptions">
- <div>
- <my-peertube-checkbox
- inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
- i18n-labelText labelText="Start at"
- ></my-peertube-checkbox>
+ <div class="options-row" *ngIf="displayOptions">
+ <div>
+ <my-peertube-checkbox
+ inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
+ i18n-labelText labelText="Start at"
+ ></my-peertube-checkbox>
- <my-timestamp-input
- [timestamp]="timestampOptions.startTimestamp"
- [maxTimestamp]="video.duration"
- [disabled]="!timestampOptions.startTimestampEnabled"
- [(ngModel)]="timestampOptions.startTimestamp"
- ></my-timestamp-input>
- </div>
+ <my-timestamp-input
+ [timestamp]="timestampOptions.startTimestamp"
+ [maxTimestamp]="video.duration"
+ [disabled]="!timestampOptions.startTimestampEnabled"
+ [(ngModel)]="timestampOptions.startTimestamp"
+ ></my-timestamp-input>
+ </div>
- <div>
- <my-peertube-checkbox
- inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
- i18n-labelText labelText="Stop at"
- ></my-peertube-checkbox>
+ <div>
+ <my-peertube-checkbox
+ inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
+ i18n-labelText labelText="Stop at"
+ ></my-peertube-checkbox>
- <my-timestamp-input
- [timestamp]="timestampOptions.stopTimestamp"
- [maxTimestamp]="video.duration"
- [disabled]="!timestampOptions.stopTimestampEnabled"
- [(ngModel)]="timestampOptions.stopTimestamp"
- ></my-timestamp-input>
+ <my-timestamp-input
+ [timestamp]="timestampOptions.stopTimestamp"
+ [maxTimestamp]="video.duration"
+ [disabled]="!timestampOptions.stopTimestampEnabled"
+ [(ngModel)]="timestampOptions.stopTimestamp"
+ ></my-timestamp-input>
+ </div>
</div>
</div>
-</div>
-<div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
- <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist"></my-peertube-checkbox>
+ <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
+ <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist"></my-peertube-checkbox>
- <div class="display-name">
- {{ playlist.displayName }}
+ <div class="display-name">
+ {{ playlist.displayName }}
- <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info">
- {{ formatTimestamp(playlist) }}
+ <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info">
+ {{ formatTimestamp(playlist) }}
+ </div>
</div>
</div>
-</div>
-<div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened">
- <my-global-icon iconName="add"></my-global-icon>
+ <div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened">
+ <my-global-icon iconName="add"></my-global-icon>
- Create a new playlist
-</div>
+ Create a new playlist
+ </div>
-<form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form">
- <div class="form-group">
- <label i18n for="displayName">Display name</label>
- <input
- type="text" id="displayName"
- formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
- >
- <div *ngIf="formErrors['displayName']" class="form-error">
- {{ formErrors['displayName'] }}
+ <form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form">
+ <div class="form-group">
+ <label i18n for="displayName">Display name</label>
+ <input
+ type="text" id="displayName"
+ formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
+ >
+ <div *ngIf="formErrors['displayName']" class="form-error">
+ {{ formErrors['displayName'] }}
+ </div>
</div>
- </div>
- <input type="submit" i18n-value value="Create" [disabled]="!form.valid">
-</form>
+ <input type="submit" i18n-value value="Create" [disabled]="!form.valid">
+ </form>
+</div>
@import '_variables';
@import '_mixins';
+.root {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
.header {
min-width: 240px;
padding: 6px 24px 10px 24px;
export class VideoAddToPlaylistComponent extends FormReactive implements OnInit {
@Input() video: Video
@Input() currentVideoTimestamp: number
+ @Input() lazyLoad = false
isNewPlaylistBlockOpened = false
videoPlaylists: PlaylistSummary[] = []
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)
-<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">
<div class="moderation-block" *ngIf="displayModerationBlock">
<my-peertube-checkbox
(change)="toggleModerationDisplay()"
- inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos"
+ inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos"
>
</my-peertube-checkbox>
</div>
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>
syndicationItems: Syndication[] = []
loadOnInit = true
- marginContent = true
videos: Video[] = []
ownerDisplayType: OwnerDisplayType = 'account'
displayModerationBlock = false
titleTooltip: string
+ displayVideoActions = true
disabled = false
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 */ }
--- /dev/null
+<ng-template #modal>
+ <div class="modal-header">
+ <h4 i18n class="modal-title">Blacklist video</h4>
+ <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+ </div>
+
+ <div class="modal-body">
+
+ <form novalidate [formGroup]="form" (ngSubmit)="blacklist()">
+ <div class="form-group">
+ <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
+ </textarea>
+ <div *ngIf="formErrors.reason" class="form-error">
+ {{ formErrors.reason }}
+ </div>
+ </div>
+
+ <div class="form-group" *ngIf="video.isLocal">
+ <my-peertube-checkbox
+ inputName="unfederate" formControlName="unfederate"
+ i18n-labelText labelText="Unfederate the video (ask for its deletion from the remote instances)"
+ ></my-peertube-checkbox>
+ </div>
+
+ <div class="form-group inputs">
+ <span i18n class="action-button action-button-cancel" (click)="hide()">
+ Cancel
+ </span>
+
+ <input
+ type="submit" i18n-value value="Submit" class="action-button-submit"
+ [disabled]="!form.valid"
+ >
+ </div>
+ </form>
+
+ </div>
+</ng-template>
--- /dev/null
+@import 'variables';
+@import 'mixins';
+
+textarea {
+ @include peertube-textarea(100%, 100px);
+}
--- /dev/null
+import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
+import { Notifier, RedirectService } from '@app/core'
+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',
+ templateUrl: './video-blacklist.component.html',
+ styleUrls: [ './video-blacklist.component.scss' ]
+})
+export class VideoBlacklistComponent extends FormReactive implements OnInit {
+ @Input() video: VideoDetails = null
+
+ @ViewChild('modal') modal: NgbModal
+
+ @Output() videoBlacklisted = new EventEmitter()
+
+ error: string = null
+
+ private openedModal: NgbModalRef
+
+ constructor (
+ protected formValidatorService: FormValidatorService,
+ private modalService: NgbModal,
+ private videoBlacklistValidatorsService: VideoBlacklistValidatorsService,
+ private videoBlacklistService: VideoBlacklistService,
+ private notifier: Notifier,
+ private redirectService: RedirectService,
+ private i18n: I18n
+ ) {
+ super()
+ }
+
+ ngOnInit () {
+ const defaultValues = { unfederate: 'true' }
+
+ this.buildForm({
+ reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON,
+ unfederate: null
+ }, defaultValues)
+ }
+
+ show () {
+ this.openedModal = this.modalService.open(this.modal, { keyboard: false })
+ }
+
+ hide () {
+ this.openedModal.close()
+ this.openedModal = null
+ }
+
+ blacklist () {
+ const reason = this.form.value[ 'reason' ] || undefined
+ const unfederate = this.video.isLocal ? this.form.value[ 'unfederate' ] : undefined
+
+ this.videoBlacklistService.blacklistVideo(this.video.id, reason, unfederate)
+ .subscribe(
+ () => {
+ this.notifier.success(this.i18n('Video blacklisted.'))
+ this.hide()
+
+ this.video.blacklisted = true
+ this.video.blacklistedReason = reason
+
+ this.videoBlacklisted.emit()
+ },
+
+ err => this.notifier.error(err.message)
+ )
+ }
+}
--- /dev/null
+<ng-template #modal let-hide="close">
+ <div class="modal-header">
+ <h4 i18n class="modal-title">Download video</h4>
+ <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+ </div>
+
+ <div class="modal-body">
+ <div class="form-group">
+ <div class="input-group input-group-sm">
+ <div class="input-group-prepend peertube-select-container">
+ <select [(ngModel)]="resolutionId">
+ <option *ngFor="let file of video.files" [value]="file.resolution.id">{{ file.resolution.label }}</option>
+ </select>
+ </div>
+ <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
+ <div class="input-group-append">
+ <button [ngxClipboard]="urlInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
+ <span class="glyphicon glyphicon-copy"></span>
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div class="download-type">
+ <div class="peertube-radio-container">
+ <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct">
+ <label i18n for="download-direct">Direct download</label>
+ </div>
+
+ <div class="peertube-radio-container">
+ <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent">
+ <label i18n for="download-torrent">Torrent (.torrent file)</label>
+ </div>
+
+ <div class="peertube-radio-container">
+ <input type="radio" name="download" id="download-magnet" [(ngModel)]="downloadType" value="magnet">
+ <label i18n for="download-magnet">Torrent (magnet link)</label>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer inputs">
+ <span i18n class="action-button action-button-cancel" (click)="hide()">
+ Cancel
+ </span>
+
+ <input
+ type="submit" i18n-value value="Download" class="action-button-submit"
+ (click)="download()"
+ >
+ </div>
+</ng-template>
--- /dev/null
+@import 'variables';
+@import 'mixins';
+
+.peertube-select-container {
+ @include peertube-select-container(100px);
+
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-right: none;
+
+ select {
+ height: inherit;
+ }
+}
+
+.download-type {
+ margin-top: 30px;
+
+ .peertube-radio-container {
+ @include peertube-radio-container;
+
+ display: inline-block;
+ margin-right: 30px;
+ }
+}
--- /dev/null
+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'
+import { Notifier } from '@app/core'
+
+@Component({
+ selector: 'my-video-download',
+ templateUrl: './video-download.component.html',
+ styleUrls: [ './video-download.component.scss' ]
+})
+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
+ ) { }
+
+ 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
+ }
+
+ onClose () {
+ this.video = undefined
+ }
+
+ download () {
+ window.location.assign(this.getLink())
+ }
+
+ getLink () {
+ // HTML select send us a string, so convert it to a number
+ this.resolutionId = parseInt(this.resolutionId.toString(), 10)
+
+ const file = this.video.files.find(f => f.resolution.id === this.resolutionId)
+ if (!file) {
+ console.error('Could not find file with resolution %d.', this.resolutionId)
+ return
+ }
+
+ switch (this.downloadType) {
+ case 'direct':
+ return file.fileDownloadUrl
+
+ case 'torrent':
+ return file.torrentDownloadUrl
+
+ case 'magnet':
+ return file.magnetUri
+ }
+ }
+
+ activateCopiedMessage () {
+ this.notifier.success(this.i18n('Copied'))
+ }
+}
--- /dev/null
+<ng-template #modal>
+ <div class="modal-header">
+ <h4 i18n class="modal-title">Report video</h4>
+ <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+ </div>
+
+ <div class="modal-body">
+
+ <div i18n class="information">
+ Your report will be sent to moderators of {{ currentHost }}.
+ <ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container>
+ </div>
+
+ <form novalidate [formGroup]="form" (ngSubmit)="report()">
+ <div class="form-group">
+ <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
+ </textarea>
+ <div *ngIf="formErrors.reason" class="form-error">
+ {{ formErrors.reason }}
+ </div>
+ </div>
+
+ <div class="form-group inputs">
+ <span i18n class="action-button action-button-cancel" (click)="hide()">
+ Cancel
+ </span>
+
+ <input
+ type="submit" i18n-value value="Submit" class="action-button-submit"
+ [disabled]="!form.valid"
+ >
+ </div>
+ </form>
+
+ </div>
+</ng-template>
--- /dev/null
+@import 'variables';
+@import 'mixins';
+
+.information {
+ margin-bottom: 20px;
+}
+
+textarea {
+ @include peertube-textarea(100%, 100px);
+}
--- /dev/null
+import { Component, Input, OnInit, ViewChild } from '@angular/core'
+import { Notifier } from '@app/core'
+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',
+ templateUrl: './video-report.component.html',
+ styleUrls: [ './video-report.component.scss' ]
+})
+export class VideoReportComponent extends FormReactive implements OnInit {
+ @Input() video: VideoDetails = null
+
+ @ViewChild('modal') modal: NgbModal
+
+ error: string = null
+
+ private openedModal: NgbModalRef
+
+ constructor (
+ protected formValidatorService: FormValidatorService,
+ private modalService: NgbModal,
+ private videoAbuseValidatorsService: VideoAbuseValidatorsService,
+ private videoAbuseService: VideoAbuseService,
+ private notifier: Notifier,
+ private i18n: I18n
+ ) {
+ super()
+ }
+
+ get currentHost () {
+ return window.location.host
+ }
+
+ get originHost () {
+ if (this.isRemoteVideo()) {
+ return this.video.account.host
+ }
+
+ return ''
+ }
+
+ ngOnInit () {
+ this.buildForm({
+ reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON
+ })
+ }
+
+ show () {
+ this.openedModal = this.modalService.open(this.modal, { keyboard: false })
+ }
+
+ hide () {
+ this.openedModal.close()
+ this.openedModal = null
+ }
+
+ report () {
+ const reason = this.form.value['reason']
+
+ this.videoAbuseService.reportVideo(this.video.id, reason)
+ .subscribe(
+ () => {
+ this.notifier.success(this.i18n('Video reported.'))
+ this.hide()
+ },
+
+ err => this.notifier.error(err.message)
+ )
+ }
+
+ isRemoteVideo () {
+ return !this.video.isLocal
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+.playlist-dropdown {
+ position: absolute;
+
+ .anchor {
+ display: block;
+ opacity: 0;
+ }
+}
+
+/deep/ .icon-playlist-add {
+ left: 2px;
+}
--- /dev/null
+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'
+ }
+ ])
+ }
+ }
+}
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
-<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow }">
+<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow }" (mouseenter)="loadActions()">
<my-video-thumbnail [video]="video" [nsfw]="isVideoBlur"></my-video-thumbnail>
- <div class="video-miniature-information">
- <a
- tabindex="-1"
- class="video-miniature-name"
- [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
- >
- <ng-container *ngIf="displayOptions.privacyLabel">
- <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span>
- <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span>
- </ng-container>
-
- {{ video.name }}
- </a>
-
- <span class="video-miniature-created-at-views">
- <ng-container *ngIf="displayOptions.date">{{ video.publishedAt | myFromNow }}</ng-container>
- <ng-container *ngIf="displayOptions.date && displayOptions.views"> - </ng-container>
- <ng-container i18n *ngIf="displayOptions.views">{{ video.views | myNumberFormatter }} views</ng-container>
- </span>
-
- <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]">
- {{ video.byAccount }}
- </a>
- <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
- {{ video.byVideoChannel }}
- </a>
-
- <div class="video-info-privacy">
- <ng-container *ngIf="displayOptions.privacyText">{{ video.privacy.label }}</ng-container>
- <ng-container *ngIf="displayOptions.privacyText && displayOptions.state"> - </ng-container>
- <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container>
+ <div class="video-bottom">
+ <div class="video-miniature-information">
+ <a
+ tabindex="-1"
+ class="video-miniature-name"
+ [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
+ >
+ <ng-container *ngIf="displayOptions.privacyLabel">
+ <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span>
+ <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span>
+ </ng-container>
+
+ {{ video.name }}
+ </a>
+
+ <span class="video-miniature-created-at-views">
+ <ng-container *ngIf="displayOptions.date">{{ video.publishedAt | myFromNow }}</ng-container>
+ <ng-container *ngIf="displayOptions.date && displayOptions.views"> - </ng-container>
+ <ng-container i18n *ngIf="displayOptions.views">{{ video.views | myNumberFormatter }} views</ng-container>
+ </span>
+
+ <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]">
+ {{ video.byAccount }}
+ </a>
+ <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
+ {{ video.byVideoChannel }}
+ </a>
+
+ <div class="video-info-privacy">
+ <ng-container *ngIf="displayOptions.privacyText">{{ video.privacy.label }}</ng-container>
+ <ng-container *ngIf="displayOptions.privacyText && displayOptions.state"> - </ng-container>
+ <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container>
+ </div>
+
+ <div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blacklisted">
+ <span class="blacklisted-label" i18n>Blacklisted</span>
+ <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
+ </div>
+
+ <div i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw">
+ Sensitive
+ </div>
</div>
- <div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blacklisted">
- <span class="blacklisted-label" i18n>Blacklisted</span>
- <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
+ <div class="video-actions">
+ <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown -->
+ <my-video-actions-dropdown
+ *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left"
+ (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()"
+ ></my-video-actions-dropdown>
</div>
-
- <div i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw">
- Sensitive
- </div>
-
</div>
</div>
}
}
+ .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;
}
}
+ .video-bottom .video-actions {
+ margin: 0;
+ top: -3px;
+ }
+
@media screen and (max-width: $small-view) {
flex-direction: column;
height: auto;
-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 = {
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
}
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
}
}
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'
+ }
+ }
}
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
// 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))
+ }
}
<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">
+++ /dev/null
-<ng-template #modal>
- <div class="modal-header">
- <h4 i18n class="modal-title">Blacklist video</h4>
- <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
- </div>
-
- <div class="modal-body">
-
- <form novalidate [formGroup]="form" (ngSubmit)="blacklist()">
- <div class="form-group">
- <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
- </textarea>
- <div *ngIf="formErrors.reason" class="form-error">
- {{ formErrors.reason }}
- </div>
- </div>
-
- <div class="form-group" *ngIf="video.isLocal">
- <my-peertube-checkbox
- inputName="unfederate" formControlName="unfederate"
- i18n-labelText labelText="Unfederate the video (ask for its deletion from the remote instances)"
- ></my-peertube-checkbox>
- </div>
-
- <div class="form-group inputs">
- <span i18n class="action-button action-button-cancel" (click)="hide()">
- Cancel
- </span>
-
- <input
- type="submit" i18n-value value="Submit" class="action-button-submit"
- [disabled]="!form.valid"
- >
- </div>
- </form>
-
- </div>
-</ng-template>
+++ /dev/null
-@import 'variables';
-@import 'mixins';
-
-textarea {
- @include peertube-textarea(100%, 100px);
-}
+++ /dev/null
-import { Component, Input, OnInit, ViewChild } from '@angular/core'
-import { Notifier, RedirectService } from '@app/core'
-import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index'
-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'
-
-@Component({
- selector: 'my-video-blacklist',
- templateUrl: './video-blacklist.component.html',
- styleUrls: [ './video-blacklist.component.scss' ]
-})
-export class VideoBlacklistComponent extends FormReactive implements OnInit {
- @Input() video: VideoDetails = null
-
- @ViewChild('modal') modal: NgbModal
-
- error: string = null
-
- private openedModal: NgbModalRef
-
- constructor (
- protected formValidatorService: FormValidatorService,
- private modalService: NgbModal,
- private videoBlacklistValidatorsService: VideoBlacklistValidatorsService,
- private videoBlacklistService: VideoBlacklistService,
- private notifier: Notifier,
- private redirectService: RedirectService,
- private i18n: I18n
- ) {
- super()
- }
-
- ngOnInit () {
- const defaultValues = { unfederate: 'true' }
-
- this.buildForm({
- reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON,
- unfederate: null
- }, defaultValues)
- }
-
- show () {
- this.openedModal = this.modalService.open(this.modal, { keyboard: false })
- }
-
- hide () {
- this.openedModal.close()
- this.openedModal = null
- }
-
- blacklist () {
- const reason = this.form.value[ 'reason' ] || undefined
- const unfederate = this.video.isLocal ? this.form.value[ 'unfederate' ] : undefined
-
- this.videoBlacklistService.blacklistVideo(this.video.id, reason, unfederate)
- .subscribe(
- () => {
- this.notifier.success(this.i18n('Video blacklisted.'))
- this.hide()
- this.redirectService.redirectToHomepage()
- },
-
- err => this.notifier.error(err.message)
- )
- }
-}
+++ /dev/null
-<ng-template #modal let-hide="close">
- <div class="modal-header">
- <h4 i18n class="modal-title">Download video</h4>
- <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
- </div>
-
- <div class="modal-body">
- <div class="form-group">
- <div class="input-group input-group-sm">
- <div class="input-group-prepend peertube-select-container">
- <select [(ngModel)]="resolutionId">
- <option *ngFor="let file of video.files" [value]="file.resolution.id">{{ file.resolution.label }}</option>
- </select>
- </div>
- <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
- <div class="input-group-append">
- <button [ngxClipboard]="urlInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
- <span class="glyphicon glyphicon-copy"></span>
- </button>
- </div>
- </div>
- </div>
-
- <div class="download-type">
- <div class="peertube-radio-container">
- <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct">
- <label i18n for="download-direct">Direct download</label>
- </div>
-
- <div class="peertube-radio-container">
- <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent">
- <label i18n for="download-torrent">Torrent (.torrent file)</label>
- </div>
-
- <div class="peertube-radio-container">
- <input type="radio" name="download" id="download-magnet" [(ngModel)]="downloadType" value="magnet">
- <label i18n for="download-magnet">Torrent (magnet link)</label>
- </div>
- </div>
- </div>
-
- <div class="modal-footer inputs">
- <span i18n class="action-button action-button-cancel" (click)="hide()">
- Cancel
- </span>
-
- <input
- type="submit" i18n-value value="Download" class="action-button-submit"
- (click)="download()"
- >
- </div>
-</ng-template>
+++ /dev/null
-@import 'variables';
-@import 'mixins';
-
-.peertube-select-container {
- @include peertube-select-container(100px);
-
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- border-right: none;
-
- select {
- height: inherit;
- }
-}
-
-.download-type {
- margin-top: 30px;
-
- .peertube-radio-container {
- @include peertube-radio-container;
-
- display: inline-block;
- margin-right: 30px;
- }
-}
+++ /dev/null
-import { Component, ElementRef, Input, OnInit, 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'
-import { Notifier } from '@app/core'
-
-@Component({
- selector: 'my-video-download',
- templateUrl: './video-download.component.html',
- styleUrls: [ './video-download.component.scss' ]
-})
-export class VideoDownloadComponent implements OnInit {
- @Input() video: VideoDetails = null
-
- @ViewChild('modal') modal: ElementRef
-
- downloadType: 'direct' | 'torrent' | 'magnet' = 'torrent'
- resolutionId: number | string = -1
-
- constructor (
- private notifier: Notifier,
- private modalService: NgbModal,
- private i18n: I18n
- ) { }
-
- ngOnInit () {
- this.resolutionId = this.video.files[0].resolution.id
- }
-
- show () {
- this.modalService.open(this.modal)
- }
-
- download () {
- window.location.assign(this.getLink())
- }
-
- getLink () {
- // HTML select send us a string, so convert it to a number
- this.resolutionId = parseInt(this.resolutionId.toString(), 10)
-
- const file = this.video.files.find(f => f.resolution.id === this.resolutionId)
- if (!file) {
- console.error('Could not find file with resolution %d.', this.resolutionId)
- return
- }
-
- const link = (() => {
- switch (this.downloadType) {
- case 'direct': {
- return file.fileDownloadUrl
- }
- case 'torrent': {
- return file.torrentDownloadUrl
- }
- case 'magnet': {
- return file.magnetUri
- }
- }
- })()
-
- return link
- }
-
- activateCopiedMessage () {
- this.notifier.success(this.i18n('Copied'))
- }
-}
+++ /dev/null
-<ng-template #modal>
- <div class="modal-header">
- <h4 i18n class="modal-title">Report video</h4>
- <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
- </div>
-
- <div class="modal-body">
-
- <div i18n class="information">
- Your report will be sent to moderators of {{ currentHost }}.
- <ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container>
- </div>
-
- <form novalidate [formGroup]="form" (ngSubmit)="report()">
- <div class="form-group">
- <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
- </textarea>
- <div *ngIf="formErrors.reason" class="form-error">
- {{ formErrors.reason }}
- </div>
- </div>
-
- <div class="form-group inputs">
- <span i18n class="action-button action-button-cancel" (click)="hide()">
- Cancel
- </span>
-
- <input
- type="submit" i18n-value value="Submit" class="action-button-submit"
- [disabled]="!form.valid"
- >
- </div>
- </form>
-
- </div>
-</ng-template>
+++ /dev/null
-@import 'variables';
-@import 'mixins';
-
-.information {
- margin-bottom: 20px;
-}
-
-textarea {
- @include peertube-textarea(100%, 100px);
-}
+++ /dev/null
-import { Component, Input, OnInit, ViewChild } from '@angular/core'
-import { Notifier } from '@app/core'
-import { FormReactive, VideoAbuseService } from '../../../shared/index'
-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'
-
-@Component({
- selector: 'my-video-report',
- templateUrl: './video-report.component.html',
- styleUrls: [ './video-report.component.scss' ]
-})
-export class VideoReportComponent extends FormReactive implements OnInit {
- @Input() video: VideoDetails = null
-
- @ViewChild('modal') modal: NgbModal
-
- error: string = null
-
- private openedModal: NgbModalRef
-
- constructor (
- protected formValidatorService: FormValidatorService,
- private modalService: NgbModal,
- private videoAbuseValidatorsService: VideoAbuseValidatorsService,
- private videoAbuseService: VideoAbuseService,
- private notifier: Notifier,
- private i18n: I18n
- ) {
- super()
- }
-
- get currentHost () {
- return window.location.host
- }
-
- get originHost () {
- if (this.isRemoteVideo()) {
- return this.video.account.host
- }
-
- return ''
- }
-
- ngOnInit () {
- this.buildForm({
- reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON
- })
- }
-
- show () {
- this.openedModal = this.modalService.open(this.modal, { keyboard: false })
- }
-
- hide () {
- this.openedModal.close()
- this.openedModal = null
- }
-
- report () {
- const reason = this.form.value['reason']
-
- this.videoAbuseService.reportVideo(this.video.id, reason)
- .subscribe(
- () => {
- this.notifier.success(this.i18n('Video reported.'))
- this.hide()
- },
-
- err => this.notifier.error(err.message)
- )
- }
-
- isRemoteVideo () {
- return !this.video.isLocal
- }
-}
</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
<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>
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;
}
margin-left: 3px;
}
}
-
- .action-dropdown {
- display: inline-block;
-
- .dropdown-menu .dropdown-item {
- @include dropdown-with-icon-item;
- }
- }
}
.video-info-likes-dislikes-bar {
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'
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',
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
)
}
- showReportModal (event: Event) {
- event.preventDefault()
- this.videoReportModal.show()
- }
-
showSupportModal () {
this.videoSupportModal.show()
}
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 []
return this.video.isRemovableBy(this.authService.getUser())
}
- async removeVideo (event: Event) {
- event.preventDefault()
-
- const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete'))
- if (res === false) return
-
- this.videoService.removeVideo(this.video.id)
- .subscribe(
- () => {
- this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }))
-
- // Go back to the video-list.
- this.redirectService.redirectToHomepage()
- },
-
- error => this.notifier.error(error.message)
- )
+ onVideoRemoved () {
+ this.redirectService.redirectToHomepage()
}
acceptedPrivacyConcern () {
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
declarations: [
VideoWatchComponent,
- VideoDownloadComponent,
VideoShareComponent,
- VideoReportComponent,
- VideoBlacklistComponent,
VideoSupportComponent,
VideoCommentsComponent,
VideoCommentAddComponent,
<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">
<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">
</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>