Add video miniature dropdown
authorChocobozzz <me@florianbigard.com>
Fri, 5 Apr 2019 08:52:27 +0000 (10:52 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 5 Apr 2019 08:53:09 +0000 (10:53 +0200)
45 files changed:
client/src/app/+my-account/my-account-history/my-account-history.component.html
client/src/app/search/search.component.html
client/src/app/search/search.component.ts
client/src/app/shared/buttons/action-dropdown.component.html
client/src/app/shared/buttons/action-dropdown.component.scss
client/src/app/shared/buttons/action-dropdown.component.ts
client/src/app/shared/misc/screen.service.ts
client/src/app/shared/shared.module.ts
client/src/app/shared/video-playlist/video-add-to-playlist.component.html
client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
client/src/app/shared/video/abstract-video-list.html
client/src/app/shared/video/abstract-video-list.ts
client/src/app/shared/video/modals/video-blacklist.component.html [new file with mode: 0644]
client/src/app/shared/video/modals/video-blacklist.component.scss [new file with mode: 0644]
client/src/app/shared/video/modals/video-blacklist.component.ts [new file with mode: 0644]
client/src/app/shared/video/modals/video-download.component.html [new file with mode: 0644]
client/src/app/shared/video/modals/video-download.component.scss [new file with mode: 0644]
client/src/app/shared/video/modals/video-download.component.ts [new file with mode: 0644]
client/src/app/shared/video/modals/video-report.component.html [new file with mode: 0644]
client/src/app/shared/video/modals/video-report.component.scss [new file with mode: 0644]
client/src/app/shared/video/modals/video-report.component.ts [new file with mode: 0644]
client/src/app/shared/video/video-actions-dropdown.component.html [new file with mode: 0644]
client/src/app/shared/video/video-actions-dropdown.component.scss [new file with mode: 0644]
client/src/app/shared/video/video-actions-dropdown.component.ts [new file with mode: 0644]
client/src/app/shared/video/video-details.model.ts
client/src/app/shared/video/video-miniature.component.html
client/src/app/shared/video/video-miniature.component.scss
client/src/app/shared/video/video-miniature.component.ts
client/src/app/shared/video/video.model.ts
client/src/app/shared/video/videos-selection.component.html
client/src/app/videos/+video-watch/modal/video-blacklist.component.html [deleted file]
client/src/app/videos/+video-watch/modal/video-blacklist.component.scss [deleted file]
client/src/app/videos/+video-watch/modal/video-blacklist.component.ts [deleted file]
client/src/app/videos/+video-watch/modal/video-download.component.html [deleted file]
client/src/app/videos/+video-watch/modal/video-download.component.scss [deleted file]
client/src/app/videos/+video-watch/modal/video-download.component.ts [deleted file]
client/src/app/videos/+video-watch/modal/video-report.component.html [deleted file]
client/src/app/videos/+video-watch/modal/video-report.component.scss [deleted file]
client/src/app/videos/+video-watch/modal/video-report.component.ts [deleted file]
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.scss
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/app/videos/+video-watch/video-watch.module.ts
client/src/app/videos/video-list/video-overview.component.html

index 4b94490a09196571f8ad49d53fbc412b2cf3a8fe..6e274f6898d8de13de6b8a086424d794cb9fee7f 100644 (file)
@@ -15,6 +15,8 @@
 
 <div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos">
   <div class="video" *ngFor="let video of videos">
-    <my-video-miniature [video]="video" [displayAsRow]="true"></my-video-miniature>
+    <my-video-miniature
+      [video]="video" [displayAsRow]="true"
+      (videoRemoved)="removeVideoFromArray(video)" (videoBlacklisted)="removeVideoFromArray(video)"></my-video-miniature>
   </div>
 </div>
index da2ace54de3790ed8d8de52e4ec4a171416f5ba5..0a9f78cb26c55ed7fa4e913d7ff05bf9e8455192 100644 (file)
     </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>
 
index a3383ed8a08f138c92e74f14b650f4fb3bcc79ec..a7ddbe1f8fb78437c3b8fd98fd67aaf9e08324e0 100644 (file)
@@ -1,6 +1,6 @@
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
-import { AuthService, Notifier, ServerService } from '@app/core'
+import { AuthService, Notifier } from '@app/core'
 import { forkJoin, Subscription } from 'rxjs'
 import { SearchService } from '@app/search/search.service'
 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
@@ -138,6 +138,10 @@ export class SearchComponent implements OnInit, OnDestroy {
     return this.advancedSearch.size()
   }
 
+  removeVideoFromArray (video: Video) {
+    this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id)
+  }
+
   private resetPagination () {
     this.pagination.currentPage = 1
     this.pagination.totalItems = null
index 6999474d64bccb0bdaaeb86f0229cfc0395305c0..cc244dc760ad5f710b6ddf7ec9b25cb5170c54df 100644 (file)
@@ -1,9 +1,11 @@
 <div class="dropdown-root" ngbDropdown [placement]="placement">
   <div
-    class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }"
+    class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange', 'button-styled': buttonStyled }"
     ngbDropdownToggle role="button"
   >
-    <my-global-icon *ngIf="!label" class="more-icon" iconName="more-horizontal"></my-global-icon>
+    <my-global-icon *ngIf="!label && buttonDirection === 'horizontal'" class="more-icon" iconName="more-horizontal"></my-global-icon>
+    <my-global-icon *ngIf="!label && buttonDirection === 'vertical'" class="more-icon" iconName="more-vertical"></my-global-icon>
+
     <span *ngIf="label" class="dropdown-toggle">{{ label }}</span>
   </div>
 
 
       <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>
index 985b2ca8844da898537e0e4a69b544295c71abbe..5073190b0bd36c1c12360879916728eb458305d2 100644 (file)
@@ -8,12 +8,19 @@
 .action-button {
   @include peertube-button;
 
-  &.grey {
-    @include grey-button;
-  }
+  &.button-styled {
+
+    &.grey {
+      @include grey-button;
+    }
+
+    &.orange {
+      @include orange-button;
+    }
 
-  &.orange {
-    @include orange-button;
+    &:hover, &:active, &:focus {
+      background-color: $grey-background-color;
+    }
   }
 
   display: inline-block;
     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%;
index 275e2b51ecfbabd82e4816b18c3414f8e9a60dd7..f5345831b86dd5827d28a11a14411347945ecdc1 100644 (file)
@@ -1,12 +1,18 @@
 import { Component, Input } from '@angular/core'
+import { GlobalIconName } from '@app/shared/images/global-icon.component'
 
 export type DropdownAction<T> = {
   label?: string
+  iconName?: GlobalIconName
   handler?: (a: T) => any
   linkBuilder?: (a: T) => (string | number)[]
   isDisplayed?: (a: T) => boolean
 }
 
+export type DropdownButtonSize = 'normal' | 'small'
+export type DropdownTheme = 'orange' | 'grey'
+export type DropdownDirection = 'horizontal' | 'vertical'
+
 @Component({
   selector: 'my-action-dropdown',
   styleUrls: [ './action-dropdown.component.scss' ],
@@ -16,14 +22,29 @@ export type DropdownAction<T> = {
 export class ActionDropdownComponent<T> {
   @Input() actions: DropdownAction<T>[] | DropdownAction<T>[][] = []
   @Input() entry: T
+
   @Input() placement = 'bottom-left'
-  @Input() buttonSize: 'normal' | 'small' = 'normal'
+
+  @Input() buttonSize: DropdownButtonSize = 'normal'
+  @Input() buttonDirection: DropdownDirection = 'horizontal'
+  @Input() buttonStyled = true
+
   @Input() label: string
-  @Input() theme: 'orange' | 'grey' = 'grey'
+  @Input() theme: DropdownTheme = 'grey'
 
   getActions () {
     if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions
 
     return [ this.actions ]
   }
+
+  areActionsDisplayed (actions: DropdownAction<T>[], entry: T) {
+    return actions.some(a => a.isDisplayed === undefined || a.isDisplayed(entry))
+  }
+
+  handleClick (event: Event, action: DropdownAction<T>) {
+    event.preventDefault()
+
+    // action.handler(entry)
+  }
 }
index 1cbc96b14b857468746111fb7c96f5717f91ed4f..db481204e5ced7d75e971ae7821c9c62bd8f0ade 100644 (file)
@@ -32,6 +32,8 @@ export class ScreenService {
   }
 
   private cacheWindowInnerWidthExpired () {
+    if (!this.lastFunctionCallTime) return true
+
     return new Date().getTime() > (this.lastFunctionCallTime + this.cacheForMs)
   }
 }
index 68225b457fbf6351d58fbd7ef1deb3b2db531b82..ded65653f224ee1e93c7cbd1c289dbe62b49d6c7 100644 (file)
@@ -80,6 +80,11 @@ import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe'
 import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
 import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
 import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
+import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component'
+import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component'
+import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component'
+import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
+import { ClipboardModule } from 'ngx-clipboard'
 
 @NgModule({
   imports: [
@@ -95,6 +100,8 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
     NgbTabsetModule,
     NgbTooltipModule,
 
+    ClipboardModule,
+
     PrimeSharedModule,
     InputMaskModule,
     NgPipesModule
@@ -110,6 +117,11 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
     VideoAddToPlaylistComponent,
     VideoPlaylistElementMiniatureComponent,
     VideosSelectionComponent,
+    VideoActionsDropdownComponent,
+
+    VideoDownloadComponent,
+    VideoReportComponent,
+    VideoBlacklistComponent,
 
     FeedComponent,
 
@@ -158,6 +170,8 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
     NgbTabsetModule,
     NgbTooltipModule,
 
+    ClipboardModule,
+
     PrimeSharedModule,
     InputMaskModule,
     BytesPipe,
@@ -172,6 +186,11 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
     VideoAddToPlaylistComponent,
     VideoPlaylistElementMiniatureComponent,
     VideosSelectionComponent,
+    VideoActionsDropdownComponent,
+
+    VideoDownloadComponent,
+    VideoReportComponent,
+    VideoBlacklistComponent,
 
     FeedComponent,
 
index 19b326206a52bb1b2efb0e7392596cfa7b7cf2b6..6029b364850208b361638e64f7d4481d54ae2969 100644 (file)
@@ -1,74 +1,76 @@
-<div class="header">
-  <div class="first-row">
-    <div i18n class="title">Save to</div>
+<div class="root">
+  <div class="header">
+    <div class="first-row">
+      <div i18n class="title">Save to</div>
 
-    <div class="options" (click)="displayOptions = !displayOptions">
-      <my-global-icon iconName="cog"></my-global-icon>
+      <div class="options" (click)="displayOptions = !displayOptions">
+        <my-global-icon iconName="cog"></my-global-icon>
 
-      <span i18n>Options</span>
+        <span i18n>Options</span>
+      </div>
     </div>
-  </div>
 
-  <div class="options-row" *ngIf="displayOptions">
-    <div>
-      <my-peertube-checkbox
-        inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
-        i18n-labelText labelText="Start at"
-      ></my-peertube-checkbox>
+    <div class="options-row" *ngIf="displayOptions">
+      <div>
+        <my-peertube-checkbox
+          inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
+          i18n-labelText labelText="Start at"
+        ></my-peertube-checkbox>
 
-      <my-timestamp-input
-        [timestamp]="timestampOptions.startTimestamp"
-        [maxTimestamp]="video.duration"
-        [disabled]="!timestampOptions.startTimestampEnabled"
-        [(ngModel)]="timestampOptions.startTimestamp"
-      ></my-timestamp-input>
-    </div>
+        <my-timestamp-input
+          [timestamp]="timestampOptions.startTimestamp"
+          [maxTimestamp]="video.duration"
+          [disabled]="!timestampOptions.startTimestampEnabled"
+          [(ngModel)]="timestampOptions.startTimestamp"
+        ></my-timestamp-input>
+      </div>
 
-    <div>
-      <my-peertube-checkbox
-        inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
-        i18n-labelText labelText="Stop at"
-      ></my-peertube-checkbox>
+      <div>
+        <my-peertube-checkbox
+          inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
+          i18n-labelText labelText="Stop at"
+        ></my-peertube-checkbox>
 
-      <my-timestamp-input
-        [timestamp]="timestampOptions.stopTimestamp"
-        [maxTimestamp]="video.duration"
-        [disabled]="!timestampOptions.stopTimestampEnabled"
-        [(ngModel)]="timestampOptions.stopTimestamp"
-      ></my-timestamp-input>
+        <my-timestamp-input
+          [timestamp]="timestampOptions.stopTimestamp"
+          [maxTimestamp]="video.duration"
+          [disabled]="!timestampOptions.stopTimestampEnabled"
+          [(ngModel)]="timestampOptions.stopTimestamp"
+        ></my-timestamp-input>
+      </div>
     </div>
   </div>
-</div>
 
-<div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
-  <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist"></my-peertube-checkbox>
+  <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
+    <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist"></my-peertube-checkbox>
 
-  <div class="display-name">
-    {{ playlist.displayName }}
+    <div class="display-name">
+      {{ playlist.displayName }}
 
-    <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info">
-      {{ formatTimestamp(playlist) }}
+      <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info">
+        {{ formatTimestamp(playlist) }}
+      </div>
     </div>
   </div>
-</div>
 
-<div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened">
-  <my-global-icon iconName="add"></my-global-icon>
+  <div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened">
+    <my-global-icon iconName="add"></my-global-icon>
 
-  Create a new playlist
-</div>
+    Create a new playlist
+  </div>
 
-<form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form">
-  <div class="form-group">
-    <label i18n for="displayName">Display name</label>
-    <input
-      type="text" id="displayName"
-      formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
-    >
-    <div *ngIf="formErrors['displayName']" class="form-error">
-      {{ formErrors['displayName'] }}
+  <form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form">
+    <div class="form-group">
+      <label i18n for="displayName">Display name</label>
+      <input
+        type="text" id="displayName"
+        formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
+      >
+      <div *ngIf="formErrors['displayName']" class="form-error">
+        {{ formErrors['displayName'] }}
+      </div>
     </div>
-  </div>
 
-  <input type="submit" i18n-value value="Create" [disabled]="!form.valid">
-</form>
+    <input type="submit" i18n-value value="Create" [disabled]="!form.valid">
+  </form>
+</div>
index bc0d55912bcdea8547197cba3f5fa97adbf485bf..0424e2ee9814edcf88b3627e07963d4910be518a 100644 (file)
@@ -1,6 +1,11 @@
 @import '_variables';
 @import '_mixins';
 
+.root {
+  max-height: 300px;
+  overflow-y: auto;
+}
+
 .header {
   min-width: 240px;
   padding: 6px 24px 10px 24px;
index 705f62404daf3c2586d8d582d6c6077d8b6f1540..152f20c856014e3754ce333020daded6ef84d5bb 100644 (file)
@@ -24,6 +24,7 @@ type PlaylistSummary = {
 export class VideoAddToPlaylistComponent extends FormReactive implements OnInit {
   @Input() video: Video
   @Input() currentVideoTimestamp: number
+  @Input() lazyLoad = false
 
   isNewPlaylistBlockOpened = false
   videoPlaylists: PlaylistSummary[] = []
@@ -57,6 +58,10 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit
       displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME
     })
 
+    if (this.lazyLoad !== true) this.load()
+  }
+
+  load () {
     forkJoin([
       this.videoPlaylistService.listAccountPlaylists(this.user.account, '-updatedAt'),
       this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id)
index e134654a35a63ad88863cec4171b6885050f0920..d1b761674a4ba2cded6086bc891eb031db0211f0 100644 (file)
@@ -1,4 +1,4 @@
-<div [ngClass]="{ 'margin-content': marginContent }">
+<div class="margin-content">
   <div class="videos-header">
     <div *ngIf="titlePage" class="title-page title-page-single">
       <div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
@@ -11,7 +11,7 @@
     <div class="moderation-block" *ngIf="displayModerationBlock">
       <my-peertube-checkbox
         (change)="toggleModerationDisplay()"
-        inputName="display-unlisted-private"  i18n-labelText labelText="Display unlisted and private videos"
+        inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos"
       >
       </my-peertube-checkbox>
     </div>
     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>
index 09965012999bdf5e0a4d485c6e83eea41d922a0d..cf43d429d04790d85ed09a930550ed82aa0e5e92 100644 (file)
@@ -26,11 +26,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
   syndicationItems: Syndication[] = []
 
   loadOnInit = true
-  marginContent = true
   videos: Video[] = []
   ownerDisplayType: OwnerDisplayType = 'account'
   displayModerationBlock = false
   titleTooltip: string
+  displayVideoActions = true
 
   disabled = false
 
@@ -120,6 +120,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
     throw new Error('toggleModerationDisplay is not implemented')
   }
 
+  removeVideoFromArray (video: Video) {
+    this.videos = this.videos.filter(v => v.id !== video.id)
+  }
+
   // On videos hook for children that want to do something
   protected onMoreVideos () { /* empty */ }
 
diff --git a/client/src/app/shared/video/modals/video-blacklist.component.html b/client/src/app/shared/video/modals/video-blacklist.component.html
new file mode 100644 (file)
index 0000000..1a87bdc
--- /dev/null
@@ -0,0 +1,38 @@
+<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>
diff --git a/client/src/app/shared/video/modals/video-blacklist.component.scss b/client/src/app/shared/video/modals/video-blacklist.component.scss
new file mode 100644 (file)
index 0000000..afcdb9a
--- /dev/null
@@ -0,0 +1,6 @@
+@import 'variables';
+@import 'mixins';
+
+textarea {
+  @include peertube-textarea(100%, 100px);
+}
diff --git a/client/src/app/shared/video/modals/video-blacklist.component.ts b/client/src/app/shared/video/modals/video-blacklist.component.ts
new file mode 100644 (file)
index 0000000..4e4e8dc
--- /dev/null
@@ -0,0 +1,76 @@
+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)
+        )
+  }
+}
diff --git a/client/src/app/shared/video/modals/video-download.component.html b/client/src/app/shared/video/modals/video-download.component.html
new file mode 100644 (file)
index 0000000..2bb5d6d
--- /dev/null
@@ -0,0 +1,52 @@
+<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>
diff --git a/client/src/app/shared/video/modals/video-download.component.scss b/client/src/app/shared/video/modals/video-download.component.scss
new file mode 100644 (file)
index 0000000..3e826c3
--- /dev/null
@@ -0,0 +1,25 @@
+@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;
+  }
+}
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts
new file mode 100644 (file)
index 0000000..64aaeb3
--- /dev/null
@@ -0,0 +1,69 @@
+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'))
+  }
+}
diff --git a/client/src/app/shared/video/modals/video-report.component.html b/client/src/app/shared/video/modals/video-report.component.html
new file mode 100644 (file)
index 0000000..b9434da
--- /dev/null
@@ -0,0 +1,36 @@
+<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>
diff --git a/client/src/app/shared/video/modals/video-report.component.scss b/client/src/app/shared/video/modals/video-report.component.scss
new file mode 100644 (file)
index 0000000..4713660
--- /dev/null
@@ -0,0 +1,10 @@
+@import 'variables';
+@import 'mixins';
+
+.information {
+  margin-bottom: 20px;
+}
+
+textarea {
+  @include peertube-textarea(100%, 100px);
+}
diff --git a/client/src/app/shared/video/modals/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts
new file mode 100644 (file)
index 0000000..725dd02
--- /dev/null
@@ -0,0 +1,81 @@
+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
+  }
+}
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.html b/client/src/app/shared/video/video-actions-dropdown.component.html
new file mode 100644 (file)
index 0000000..300fe31
--- /dev/null
@@ -0,0 +1,21 @@
+<ng-container *ngIf="videoActions.length !== 0">
+
+  <div class="playlist-dropdown" ngbDropdown #playlistDropdown="ngbDropdown" role="button" autoClose="outside" [placement]="getPlaylistDropdownPlacement()"
+    *ngIf="isUserLoggedIn() && displayOptions.playlist" (openChange)="playlistAdd.openChange($event)"
+  >
+    <span class="anchor" ngbDropdownAnchor></span>
+
+    <div ngbDropdownMenu>
+      <my-video-add-to-playlist #playlistAdd [video]="video" [lazyLoad]="true"></my-video-add-to-playlist>
+    </div>
+  </div>
+
+  <my-action-dropdown
+    [actions]="videoActions" [label]="label" [entry]="{ video: video }" (mouseenter)="loadDropdownInformation()"
+    [buttonSize]="buttonSize" [placement]="placement" [buttonDirection]="buttonDirection" [buttonStyled]="buttonStyled"
+  ></my-action-dropdown>
+
+  <my-video-download #videoDownloadModal></my-video-download>
+  <my-video-report #videoReportModal [video]="video"></my-video-report>
+  <my-video-blacklist #videoBlacklistModal [video]="video" (videoBlacklisted)="onVideoBlacklisted()"></my-video-blacklist>
+</ng-container>
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.scss b/client/src/app/shared/video/video-actions-dropdown.component.scss
new file mode 100644 (file)
index 0000000..7ffdce8
--- /dev/null
@@ -0,0 +1,12 @@
+.playlist-dropdown {
+  position: absolute;
+
+  .anchor {
+    display: block;
+    opacity: 0;
+  }
+}
+
+/deep/ .icon-playlist-add {
+  left: 2px;
+}
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.ts b/client/src/app/shared/video/video-actions-dropdown.component.ts
new file mode 100644 (file)
index 0000000..90bdf7d
--- /dev/null
@@ -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'
+        }
+      ])
+    }
+  }
+}
index 38835734363a6c5040d002a45fb1ee4f6df976ed..8463e15d750491e3f5afe997acceb0c29232d5da 100644 (file)
@@ -44,22 +44,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
     this.buildLikeAndDislikePercents()
   }
 
-  isRemovableBy (user: AuthUser) {
-    return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
-  }
-
-  isBlackistableBy (user: AuthUser) {
-    return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
-  }
-
-  isUnblacklistableBy (user: AuthUser) {
-    return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
-  }
-
-  isUpdatableBy (user: AuthUser) {
-    return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
-  }
-
   buildLikeAndDislikePercents () {
     this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
     this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100
index f4ae0b0dd5d9d44c2a943f515dcf90c183729314..7af0f111380eeccb670baa5db0e507c6836783db 100644 (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>
 
-  <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>
index fdc3dc0339e6940220bbff6b9476d27bee84a865..0d4e59c2a372552de8419cf8af6a41ec33b14e92 100644 (file)
     }
   }
 
+  .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;
index 800417a7994174a55e81082aafcb47a12d8a270f..e3552abbaf4caa8297214b575e90f3ed62c2a1d1 100644 (file)
@@ -1,9 +1,11 @@
-import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'
+import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, LOCALE_ID, OnInit, Output } from '@angular/core'
 import { User } from '../users'
 import { Video } from './video.model'
 import { ServerService } from '@app/core'
 import { VideoPrivacy, VideoState } from '../../../../../shared'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component'
+import { ScreenService } from '@app/shared/misc/screen.service'
 
 export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
 export type MiniatureDisplayOptions = {
@@ -38,10 +40,26 @@ export class VideoMiniatureComponent implements OnInit {
     blacklistInfo: false
   }
   @Input() displayAsRow = false
+  @Input() displayVideoActions = true
+
+  @Output() videoBlacklisted = new EventEmitter()
+  @Output() videoUnblacklisted = new EventEmitter()
+  @Output() videoRemoved = new EventEmitter()
+
+  videoActionsDisplayOptions: VideoActionsDisplayType = {
+    playlist: true,
+    download: false,
+    update: true,
+    blacklist: true,
+    delete: true,
+    report: true
+  }
+  showActions = false
 
   private ownerDisplayTypeChosen: 'account' | 'videoChannel'
 
   constructor (
+    private screenService: ScreenService,
     private serverService: ServerService,
     private i18n: I18n,
     @Inject(LOCALE_ID) private localeId: string
@@ -52,20 +70,10 @@ export class VideoMiniatureComponent implements OnInit {
   }
 
   ngOnInit () {
-    if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') {
-      this.ownerDisplayTypeChosen = this.ownerDisplayType
-      return
-    }
+    this.setUpBy()
 
-    // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
-    // -> Use the account name
-    if (
-      this.video.channel.name === `${this.video.account.name}_channel` ||
-      this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
-    ) {
-      this.ownerDisplayTypeChosen = 'account'
-    } else {
-      this.ownerDisplayTypeChosen = 'videoChannel'
+    if (this.screenService.isInSmallView()) {
+      this.showActions = true
     }
   }
 
@@ -109,4 +117,38 @@ export class VideoMiniatureComponent implements OnInit {
 
     return ''
   }
+
+  loadActions () {
+    if (this.displayVideoActions) this.showActions = true
+  }
+
+  onVideoBlacklisted () {
+    this.videoBlacklisted.emit()
+  }
+
+  onVideoUnblacklisted () {
+    this.videoUnblacklisted.emit()
+  }
+
+  onVideoRemoved () {
+    this.videoRemoved.emit()
+  }
+
+  private setUpBy () {
+    if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') {
+      this.ownerDisplayTypeChosen = this.ownerDisplayType
+      return
+    }
+
+    // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
+    // -> Use the account name
+    if (
+      this.video.channel.name === `${this.video.account.name}_channel` ||
+      this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
+    ) {
+      this.ownerDisplayTypeChosen = 'account'
+    } else {
+      this.ownerDisplayTypeChosen = 'videoChannel'
+    }
+  }
 }
index 95b5e36713b35f3211178fe2895622c5639a1d4c..0cef3eb8f1de5f17c2992c9a6134ae0806ae366b 100644 (file)
@@ -1,11 +1,12 @@
 import { User } from '../'
-import { PlaylistElement, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
+import { PlaylistElement, UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
 import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
 import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model'
 import { durationToString, getAbsoluteAPIUrl } from '../misc/utils'
 import { peertubeTranslate, ServerConfig } from '../../../../../shared/models'
 import { Actor } from '@app/shared/actor/actor.model'
 import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
+import { AuthUser } from '@app/core'
 
 export class Video implements VideoServerModel {
   byVideoChannel: string
@@ -141,4 +142,20 @@ export class Video implements VideoServerModel {
     // Return default instance config
     return serverConfig.instance.defaultNSFWPolicy !== 'display'
   }
+
+  isRemovableBy (user: AuthUser) {
+    return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
+  }
+
+  isBlackistableBy (user: AuthUser) {
+    return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
+  }
+
+  isUnblacklistableBy (user: AuthUser) {
+    return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
+  }
+
+  isUpdatableBy (user: AuthUser) {
+    return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
+  }
 }
index 6f3401b4b9f6bc8a8bcdde1d6be47c4dec9093cb..53809b6fdb198e35d192ec8eec33fcf8ceadd90a 100644 (file)
@@ -6,7 +6,7 @@
       <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="_selection[video.id]"></my-peertube-checkbox>
     </div>
 
-    <my-video-miniature [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions"></my-video-miniature>
+    <my-video-miniature [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" [displayVideoActions]="false"></my-video-miniature>
 
     <!-- Display only once -->
     <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0">
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
deleted file mode 100644 (file)
index 1a87bdc..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<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>
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.scss b/client/src/app/videos/+video-watch/modal/video-blacklist.component.scss
deleted file mode 100644 (file)
index afcdb9a..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-@import 'variables';
-@import 'mixins';
-
-textarea {
-  @include peertube-textarea(100%, 100px);
-}
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
deleted file mode 100644 (file)
index 50a7cad..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-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)
-        )
-  }
-}
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/videos/+video-watch/modal/video-download.component.html
deleted file mode 100644 (file)
index 2bb5d6d..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-<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>
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.scss b/client/src/app/videos/+video-watch/modal/video-download.component.scss
deleted file mode 100644 (file)
index 3e826c3..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-@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;
-  }
-}
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.ts b/client/src/app/videos/+video-watch/modal/video-download.component.ts
deleted file mode 100644 (file)
index 8343857..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-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'))
-  }
-}
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html
deleted file mode 100644 (file)
index b9434da..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-<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>
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.scss b/client/src/app/videos/+video-watch/modal/video-report.component.scss
deleted file mode 100644 (file)
index 4713660..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-@import 'variables';
-@import 'mixins';
-
-.information {
-  margin-bottom: 20px;
-}
-
-textarea {
-  @include peertube-textarea(100%, 100px);
-}
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.ts b/client/src/app/videos/+video-watch/modal/video-report.component.ts
deleted file mode 100644 (file)
index 911f3b4..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-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
-  }
-}
index ad1d04b702a7160bbae7fd60b75bdfcf290556af..7755a729a176c5b663c10276aeacd05c7bf38155 100644 (file)
                   </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>
index 2874847cd31c2d6f26487197eae03bd00a69941a..c1eaf9b2b0d192917ae880bf48bd15a06abe941f 100644 (file)
@@ -257,7 +257,9 @@ $player-factor: 1.7; // 16/9
           display: flex;
           align-items: center;
 
-          .action-button:not(:first-child), .action-dropdown {
+          .action-button:not(:first-child),
+          .action-dropdown,
+          my-video-actions-dropdown {
             margin-left: 10px;
           }
 
@@ -304,14 +306,6 @@ $player-factor: 1.7; // 16/9
               margin-left: 3px;
             }
           }
-
-          .action-dropdown {
-            display: inline-block;
-
-            .dropdown-menu .dropdown-item {
-              @include dropdown-with-icon-item;
-            }
-          }
         }
 
         .video-info-likes-dislikes-bar {
index cedbbf985536ae1abcc6d256bb2d937eb38e90bd..53673d9d92a92c761a57c35360131469c2ed5086 100644 (file)
@@ -13,10 +13,7 @@ import { AuthService, ConfirmService } from '../../core'
 import { RestExtractor, VideoBlacklistService } from '../../shared'
 import { VideoDetails } from '../../shared/video/video-details.model'
 import { VideoService } from '../../shared/video/video.service'
-import { VideoDownloadComponent } from './modal/video-download.component'
-import { VideoReportComponent } from './modal/video-report.component'
 import { VideoShareComponent } from './modal/video-share.component'
-import { VideoBlacklistComponent } from './modal/video-blacklist.component'
 import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { environment } from '../../../environments/environment'
@@ -32,6 +29,7 @@ import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
 import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
 import { Video } from '@app/shared/video/video.model'
+import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component'
 
 @Component({
   selector: 'my-video-watch',
@@ -41,11 +39,8 @@ import { Video } from '@app/shared/video/video.model'
 export class VideoWatchComponent implements OnInit, OnDestroy {
   private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
 
-  @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
   @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
-  @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
   @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
-  @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
   @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
 
   player: any
@@ -212,11 +207,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
         )
   }
 
-  showReportModal (event: Event) {
-    event.preventDefault()
-    this.videoReportModal.show()
-  }
-
   showSupportModal () {
     this.videoSupportModal.show()
   }
@@ -225,54 +215,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.videoShareModal.show(this.currentTime)
   }
 
-  showDownloadModal (event: Event) {
-    event.preventDefault()
-    this.videoDownloadModal.show()
-  }
-
-  showBlacklistModal (event: Event) {
-    event.preventDefault()
-    this.videoBlacklistModal.show()
-  }
-
-  async unblacklistVideo (event: Event) {
-    event.preventDefault()
-
-    const confirmMessage = this.i18n(
-      'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
-    )
-
-    const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist'))
-    if (res === false) return
-
-    this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe(
-      () => {
-        this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name }))
-
-        this.video.blacklisted = false
-        this.video.blacklistedReason = null
-      },
-
-      err => this.notifier.error(err.message)
-    )
-  }
-
   isUserLoggedIn () {
     return this.authService.isLoggedIn()
   }
 
-  isVideoUpdatable () {
-    return this.video.isUpdatableBy(this.authService.getUser())
-  }
-
-  isVideoBlacklistable () {
-    return this.video.isBlackistableBy(this.user)
-  }
-
-  isVideoUnblacklistable () {
-    return this.video.isUnblacklistableBy(this.user)
-  }
-
   getVideoTags () {
     if (!this.video || Array.isArray(this.video.tags) === false) return []
 
@@ -283,23 +229,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     return this.video.isRemovableBy(this.authService.getUser())
   }
 
-  async removeVideo (event: Event) {
-    event.preventDefault()
-
-    const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete'))
-    if (res === false) return
-
-    this.videoService.removeVideo(this.video.id)
-        .subscribe(
-          () => {
-            this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }))
-
-            // Go back to the video-list.
-            this.redirectService.redirectToHomepage()
-          },
-
-          error => this.notifier.error(error.message)
-        )
+  onVideoRemoved () {
+    this.redirectService.redirectToHomepage()
   }
 
   acceptedPrivacyConcern () {
index 2f448db780c9000f899187d28cb39577b1590914..983350f52c2c7bf73a39f0b094876d584981ad22 100644 (file)
@@ -1,26 +1,21 @@
 import { NgModule } from '@angular/core'
 import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
-import { ClipboardModule } from 'ngx-clipboard'
 import { SharedModule } from '../../shared'
 import { VideoCommentAddComponent } from './comment/video-comment-add.component'
 import { VideoCommentComponent } from './comment/video-comment.component'
 import { VideoCommentService } from './comment/video-comment.service'
 import { VideoCommentsComponent } from './comment/video-comments.component'
-import { VideoDownloadComponent } from './modal/video-download.component'
-import { VideoReportComponent } from './modal/video-report.component'
 import { VideoShareComponent } from './modal/video-share.component'
 import { VideoWatchRoutingModule } from './video-watch-routing.module'
 import { VideoWatchComponent } from './video-watch.component'
 import { NgxQRCodeModule } from 'ngx-qrcode2'
 import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
-import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component'
 import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
 
 @NgModule({
   imports: [
     VideoWatchRoutingModule,
     SharedModule,
-    ClipboardModule,
     NgbTooltipModule,
     NgxQRCodeModule,
     RecommendationsModule
@@ -29,10 +24,7 @@ import { RecommendationsModule } from '@app/videos/recommendations/recommendatio
   declarations: [
     VideoWatchComponent,
 
-    VideoDownloadComponent,
     VideoShareComponent,
-    VideoReportComponent,
-    VideoBlacklistComponent,
     VideoSupportComponent,
     VideoCommentsComponent,
     VideoCommentAddComponent,
index cb26592e3b71223edb7d363bc103aefead224f93..b644dd7982fdb7e3f793c2513d05d2bb9df5e048 100644 (file)
@@ -7,7 +7,7 @@
       <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a>
     </div>
 
-    <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user"></my-video-miniature>
+    <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature>
   </div>
 
   <div class="section" *ngFor="let object of overview.tags">
@@ -15,7 +15,7 @@
       <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a>
     </div>
 
-    <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user"></my-video-miniature>
+    <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature>
   </div>
 
   <div class="section channel" *ngFor="let object of overview.channels">
@@ -27,7 +27,7 @@
       </a>
     </div>
 
-    <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user"></my-video-miniature>
+    <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature>
   </div>
 
 </div>