Playlist support in watch page
authorChocobozzz <me@florianbigard.com>
Wed, 13 Mar 2019 13:18:58 +0000 (14:18 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Mon, 18 Mar 2019 10:17:59 +0000 (11:17 +0100)
20 files changed:
client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html
client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss
client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts
client/src/app/shared/images/global-icon.component.ts
client/src/app/shared/shared.module.ts
client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html [new file with mode: 0644]
client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss [new file with mode: 0644]
client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts [new file with mode: 0644]
client/src/app/shared/video/infinite-scroller.directive.ts
client/src/app/shared/video/video-thumbnail.component.html
client/src/app/shared/video/video-thumbnail.component.ts
client/src/app/videos/+video-watch/video-watch-routing.module.ts
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/assets/images/global/play.html [new file with mode: 0644]
client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
client/src/assets/player/peertube-plugin.ts
client/src/sass/include/_miniature.scss
client/src/sass/include/_mixins.scss

index 67a8b1a9169a3b11285c7b0250ba20658eb812aa..bc26e198ed269c61254fda29dad5a8b6b808c7e4 100644 (file)
@@ -5,60 +5,7 @@
   cdkDropList (cdkDropListDropped)="drop($event)"
 >
   <div class="video" *ngFor="let video of videos" cdkDrag (cdkDragMoved)="onDragMove($event)">
-    <div class="position">{{ video.playlistElement.position }}</div>
-
-    <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur(video)"></my-video-thumbnail>
-
-    <div class="video-info">
-      <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
-      <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
-      <span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(video)}}</span>
-    </div>
-
-    <div class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right" (openChange)="onDropdownOpenChange()" autoClose="outside">
-      <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more"></my-global-icon>
-
-      <div ngbDropdownMenu>
-        <div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, video)">
-          <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Edit starts/stops at</ng-container>
-        </div>
-
-        <div class="timestamp-options" *ngIf="displayTimestampOptions">
-          <div>
-            <my-peertube-checkbox
-              inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
-              i18n-labelText labelText="Start at"
-            ></my-peertube-checkbox>
-
-            <my-timestamp-input
-              [timestamp]="timestampOptions.startTimestamp"
-              [maxTimestamp]="video.duration"
-              [disabled]="!timestampOptions.startTimestampEnabled"
-              [(ngModel)]="timestampOptions.startTimestamp"
-            ></my-timestamp-input>
-          </div>
-
-          <div>
-            <my-peertube-checkbox
-              inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
-              i18n-labelText labelText="Stop at"
-            ></my-peertube-checkbox>
-
-            <my-timestamp-input
-              [timestamp]="timestampOptions.stopTimestamp"
-              [maxTimestamp]="video.duration"
-              [disabled]="!timestampOptions.stopTimestampEnabled"
-              [(ngModel)]="timestampOptions.stopTimestamp"
-            ></my-timestamp-input>
-          </div>
-
-          <input type="submit" i18n-value value="Save" (click)="updateTimestamps(video)">
-        </div>
-
-        <span class="dropdown-item" (click)="removeFromPlaylist(video)">
-          <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{playlist?.displayName}}</ng-container>
-        </span>
-      </div>
-    </div>
+    <my-video-playlist-element-miniature [video]="video" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)">
+    </my-video-playlist-element-miniature>
   </div>
 </div>
index 4ac89d08f4ebb74d18c1e1090c1fc8aaf410ab1a..b05af0490d72605e32dd3006957978d5fc30cbbc 100644 (file)
@@ -2,100 +2,6 @@
 @import '_mixins';
 @import '_miniature';
 
-.video, .cdk-drag-preview {
-  display: flex;
-  align-items: center;
-  background-color: var(--mainBackgroundColor);
-  cursor: pointer;
-  padding: 10px;
-  border-bottom: 1px solid $separator-border-color;
-
-  &:hover {
-    background-color: rgba(0, 0, 0, 0.05);
-
-    .more {
-      display: block;
-    }
-  }
-
-  .position {
-    font-weight: $font-semibold;
-    margin-right: 10px;
-    color: $grey-foreground-color;
-    min-width: 20px;
-  }
-
-  my-video-thumbnail {
-    display: flex; // Avoids an issue with line-height that adds space below the element
-    margin-right: 10px;
-
-    /deep/ .video-thumbnail {
-      @include miniature-thumbnail(130px, 72px);
-    }
-  }
-
-  .video-info {
-    display: flex;
-    flex-direction: column;
-
-    a {
-      @include disable-default-a-behaviour;
-
-      color: var(--mainForegroundColor);
-    }
-
-    .video-info-name {
-      font-size: 18px;
-      font-weight: $font-semibold;
-    }
-
-    .video-info-account, .video-info-timestamp {
-      color: $grey-foreground-color;
-    }
-  }
-
-  .more {
-    justify-self: flex-end;
-    margin-left: auto;
-    cursor: pointer;
-    display: none;
-
-    &.show {
-      display: block;
-    }
-
-    .icon-more {
-      @include apply-svg-color($grey-foreground-color);
-
-      &::after {
-        border: none;
-      }
-    }
-
-    .dropdown-item {
-      @include dropdown-with-icon-item;
-    }
-
-    .timestamp-options {
-      padding-top: 0;
-      padding-left: 35px;
-      margin-bottom: 15px;
-
-      > div {
-        display: flex;
-        align-items: center;
-      }
-
-      input {
-        @include peertube-button;
-        @include orange-button;
-
-        margin-top: 10px;
-      }
-    }
-  }
-}
-
 // Thanks Angular CDK <3 https://material.angular.io/cdk/drag-drop/examples
 .cdk-drag-preview {
   box-sizing: border-box;
index 4076a3721c30a50b542182104feadcbcd029a820..dcf470be3748e3f8d7ccfcca025e41fd14cccfdc 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
+import { Component, OnDestroy, OnInit } from '@angular/core'
 import { Notifier, ServerService } from '@app/core'
 import { AuthService } from '../../core/auth'
 import { ConfirmService } from '../../core/confirm'
@@ -10,9 +10,6 @@ import { VideoService } from '@app/shared/video/video.service'
 import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
 import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { secondsToTime } from '../../../assets/player/utils'
-import { VideoPlaylistElementUpdate } from '@shared/models'
-import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
 import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop'
 import { throttleTime } from 'rxjs/operators'
 
@@ -22,8 +19,6 @@ import { throttleTime } from 'rxjs/operators'
   styleUrls: [ './my-account-video-playlist-elements.component.scss' ]
 })
 export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy {
-  @ViewChild('moreDropdown') moreDropdown: NgbDropdown
-
   videos: Video[] = []
   playlist: VideoPlaylist
 
@@ -33,15 +28,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
     totalItems: null
   }
 
-  displayTimestampOptions = false
-
-  timestampOptions: {
-    startTimestampEnabled: boolean
-    startTimestamp: number
-    stopTimestampEnabled: boolean
-    stopTimestamp: number
-  } = {} as any
-
   private videoPlaylistId: string | number
   private paramsSub: Subscription
   private dragMoveSubject = new Subject<number>()
@@ -124,45 +110,9 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
     // }
   }
 
-  isVideoBlur (video: Video) {
-    return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
-  }
-
-  removeFromPlaylist (video: Video) {
-    this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, video.id)
-        .subscribe(
-          () => {
-            this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName }))
-
-            this.videos = this.videos.filter(v => v.id !== video.id)
-            this.reorderClientPositions()
-          },
-
-          err => this.notifier.error(err.message)
-        )
-
-    this.moreDropdown.close()
-  }
-
-  updateTimestamps (video: Video) {
-    const body: VideoPlaylistElementUpdate = {}
-
-    body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null
-    body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null
-
-    this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, video.id, body)
-        .subscribe(
-          () => {
-            this.notifier.success(this.i18n('Timestamps updated'))
-
-            video.playlistElement.startTimestamp = body.startTimestamp
-            video.playlistElement.stopTimestamp = body.stopTimestamp
-          },
-
-          err => this.notifier.error(err.message)
-        )
-
-    this.moreDropdown.close()
+  onElementRemoved (video: Video) {
+    this.videos = this.videos.filter(v => v.id !== video.id)
+    this.reorderClientPositions()
   }
 
   onNearOfBottom () {
@@ -173,50 +123,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
     this.loadElements()
   }
 
-  formatTimestamp (video: Video) {
-    const start = video.playlistElement.startTimestamp
-    const stop = video.playlistElement.stopTimestamp
-
-    const startFormatted = secondsToTime(start, true, ':')
-    const stopFormatted = secondsToTime(stop, true, ':')
-
-    if (start === null && stop === null) return ''
-
-    if (start !== null && stop === null) return this.i18n('Starts at ') + startFormatted
-    if (start === null && stop !== null) return this.i18n('Stops at ') + stopFormatted
-
-    return this.i18n('Starts at ') + startFormatted + this.i18n(' and stops at ') + stopFormatted
-  }
-
-  onDropdownOpenChange () {
-    this.displayTimestampOptions = false
-  }
-
-  toggleDisplayTimestampsOptions (event: Event, video: Video) {
-    event.preventDefault()
-
-    this.displayTimestampOptions = !this.displayTimestampOptions
-
-    if (this.displayTimestampOptions === true) {
-      this.timestampOptions = {
-        startTimestampEnabled: false,
-        stopTimestampEnabled: false,
-        startTimestamp: 0,
-        stopTimestamp: video.duration
-      }
-
-      if (video.playlistElement.startTimestamp) {
-        this.timestampOptions.startTimestampEnabled = true
-        this.timestampOptions.startTimestamp = video.playlistElement.startTimestamp
-      }
-
-      if (video.playlistElement.stopTimestamp) {
-        this.timestampOptions.stopTimestampEnabled = true
-        this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp
-      }
-    }
-  }
-
   private loadElements () {
     this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
         .subscribe(({ totalVideos, videos }) => {
index 093e88033bb213c5fe7615767d1faaacaebd47f5..3fa6fea96d80ea37d503415b2914ea43b9e7c7a3 100644 (file)
@@ -27,7 +27,8 @@ const icons = {
   'more-vertical': require('../../../assets/images/global/more-vertical.html'),
   'share': require('../../../assets/images/video/share.html'),
   'upload': require('../../../assets/images/video/upload.html'),
-  'playlist-add': require('../../../assets/images/video/playlist-add.html')
+  'playlist-add': require('../../../assets/images/video/playlist-add.html'),
+  'play': require('../../../assets/images/global/play.html')
 }
 
 export type GlobalIconName = keyof typeof icons
index 05da0d82926f4a68a1cd088b764c04cb80b48bda..3647fc786f02408dfa4b87b91fdbcdf7fe9d9bae 100644 (file)
@@ -77,6 +77,7 @@ import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
 import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
 import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component'
 import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.component'
+import { VideoPlaylistElementMiniatureComponent } from '@app/shared/video-playlist/video-playlist-element-miniature.component'
 
 @NgModule({
   imports: [
@@ -105,6 +106,7 @@ import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.compo
     VideoMiniatureComponent,
     VideoPlaylistMiniatureComponent,
     VideoAddToPlaylistComponent,
+    VideoPlaylistElementMiniatureComponent,
 
     FeedComponent,
 
@@ -163,6 +165,7 @@ import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.compo
     VideoMiniatureComponent,
     VideoPlaylistMiniatureComponent,
     VideoAddToPlaylistComponent,
+    VideoPlaylistElementMiniatureComponent,
 
     FeedComponent,
 
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html
new file mode 100644 (file)
index 0000000..1f17867
--- /dev/null
@@ -0,0 +1,73 @@
+<div class="video" [ngClass]="{ playing: playing }">
+  <a [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()">
+    <div class="position">
+      <my-global-icon *ngIf="playing" iconName="play"></my-global-icon>
+      <ng-container *ngIf="!playing">{{ video.playlistElement.position }}</ng-container>
+    </div>
+
+    <my-video-thumbnail
+      [video]="video" [nsfw]="isVideoBlur(video)"
+      [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
+    ></my-video-thumbnail>
+
+    <div class="video-info">
+      <a tabindex="-1" class="video-info-name"
+         [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
+         [attr.title]="video.name"
+      >{{ video.name }}</a>
+
+      <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
+      <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ video.byAccount }}</span>
+
+      <span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(video)}}</span>
+    </div>
+  </a>
+
+  <div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right" (openChange)="onDropdownOpenChange()"
+       autoClose="outside">
+    <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon>
+
+    <div ngbDropdownMenu>
+      <div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, video)">
+        <my-global-icon iconName="edit"></my-global-icon>
+        <ng-container i18n>Edit starts/stops at</ng-container>
+      </div>
+
+      <div class="timestamp-options" *ngIf="displayTimestampOptions">
+        <div>
+          <my-peertube-checkbox
+            inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
+            i18n-labelText labelText="Start at"
+          ></my-peertube-checkbox>
+
+          <my-timestamp-input
+            [timestamp]="timestampOptions.startTimestamp"
+            [maxTimestamp]="video.duration"
+            [disabled]="!timestampOptions.startTimestampEnabled"
+            [(ngModel)]="timestampOptions.startTimestamp"
+          ></my-timestamp-input>
+        </div>
+
+        <div>
+          <my-peertube-checkbox
+            inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
+            i18n-labelText labelText="Stop at"
+          ></my-peertube-checkbox>
+
+          <my-timestamp-input
+            [timestamp]="timestampOptions.stopTimestamp"
+            [maxTimestamp]="video.duration"
+            [disabled]="!timestampOptions.stopTimestampEnabled"
+            [(ngModel)]="timestampOptions.stopTimestamp"
+          ></my-timestamp-input>
+        </div>
+
+        <input type="submit" i18n-value value="Save" (click)="updateTimestamps(video)">
+      </div>
+
+      <span class="dropdown-item" (click)="removeFromPlaylist(video)">
+            <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{playlist?.displayName}}</ng-container>
+          </span>
+    </div>
+  </div>
+</div>
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss
new file mode 100644 (file)
index 0000000..eb869f6
--- /dev/null
@@ -0,0 +1,124 @@
+@import '_variables';
+@import '_mixins';
+@import '_miniature';
+
+.video {
+  display: flex;
+  align-items: center;
+  background-color: var(--mainBackgroundColor);
+  padding: 10px;
+  border-bottom: 1px solid $separator-border-color;
+
+  &:hover {
+    background-color: rgba(0, 0, 0, 0.05);
+
+    .more {
+      display: block;
+    }
+  }
+
+  &.playing {
+    background-color: rgba(0, 0, 0, 0.02);
+  }
+
+  a {
+    @include disable-default-a-behaviour;
+
+    min-width: 0;
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    flex-grow: 1;
+
+    .position {
+      font-weight: $font-semibold;
+      margin-right: 10px;
+      color: $grey-foreground-color;
+      min-width: 20px;
+
+      my-global-icon {
+        @include apply-svg-color($grey-foreground-color);
+
+        width: 17px;
+        position: relative;
+        left: -2px;
+      }
+    }
+
+    my-video-thumbnail {
+      @include thumbnail-size-component(130px, 72px);
+
+      display: flex; // Avoids an issue with line-height that adds space below the element
+      margin-right: 10px;
+    }
+
+    .video-info {
+      display: flex;
+      flex-direction: column;
+      min-width: 0;
+
+      a {
+        color: var(--mainForegroundColor);
+        width: fit-content;
+
+        &:hover {
+          text-decoration: underline !important;
+        }
+      }
+
+      .video-info-name {
+        font-size: 18px;
+        font-weight: $font-semibold;
+
+        @include ellipsis;
+      }
+
+      .video-info-account, .video-info-timestamp {
+        color: $grey-foreground-color;
+      }
+    }
+  }
+
+  .more {
+    justify-self: flex-end;
+    margin-left: auto;
+    cursor: pointer;
+    display: none;
+
+    &.show {
+      display: block;
+    }
+
+    .icon-more {
+      @include apply-svg-color($grey-foreground-color);
+
+      display: flex;
+
+      &::after {
+        border: none;
+      }
+    }
+
+    .dropdown-item {
+      @include dropdown-with-icon-item;
+    }
+
+    .timestamp-options {
+      padding-top: 0;
+      padding-left: 35px;
+      margin-bottom: 15px;
+
+      > div {
+        display: flex;
+        align-items: center;
+      }
+
+      input {
+        @include peertube-button;
+        @include orange-button;
+
+        margin-top: 10px;
+      }
+    }
+  }
+}
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts
new file mode 100644 (file)
index 0000000..c0cfd85
--- /dev/null
@@ -0,0 +1,149 @@
+import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
+import { Video } from '@app/shared/video/video.model'
+import { VideoPlaylistElementUpdate } from '@shared/models'
+import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
+import { ActivatedRoute } from '@angular/router'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { VideoService } from '@app/shared/video/video.service'
+import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
+import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
+import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
+import { secondsToTime } from '../../../assets/player/utils'
+
+@Component({
+  selector: 'my-video-playlist-element-miniature',
+  styleUrls: [ './video-playlist-element-miniature.component.scss' ],
+  templateUrl: './video-playlist-element-miniature.component.html'
+})
+export class VideoPlaylistElementMiniatureComponent {
+  @ViewChild('moreDropdown') moreDropdown: NgbDropdown
+
+  @Input() playlist: VideoPlaylist
+  @Input() video: Video
+  @Input() owned = false
+  @Input() playing = false
+  @Input() rowLink = false
+  @Input() accountLink = true
+
+  @Output() elementRemoved = new EventEmitter<Video>()
+
+  displayTimestampOptions = false
+
+  timestampOptions: {
+    startTimestampEnabled: boolean
+    startTimestamp: number
+    stopTimestampEnabled: boolean
+    stopTimestamp: number
+  } = {} as any
+
+  constructor (
+    private authService: AuthService,
+    private serverService: ServerService,
+    private notifier: Notifier,
+    private confirmService: ConfirmService,
+    private route: ActivatedRoute,
+    private i18n: I18n,
+    private videoService: VideoService,
+    private videoPlaylistService: VideoPlaylistService
+  ) {}
+
+  buildRouterLink () {
+    if (!this.playlist) return null
+
+    return [ '/videos/watch/playlist', this.playlist.uuid ]
+  }
+
+  buildRouterQuery () {
+    if (!this.video) return {}
+
+    return {
+      videoId: this.video.uuid,
+      start: this.video.playlistElement.startTimestamp,
+      stop: this.video.playlistElement.stopTimestamp
+    }
+  }
+
+  isVideoBlur (video: Video) {
+    return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
+  }
+
+  removeFromPlaylist (video: Video) {
+    this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, video.id)
+        .subscribe(
+          () => {
+            this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName }))
+
+            this.elementRemoved.emit(this.video)
+          },
+
+          err => this.notifier.error(err.message)
+        )
+
+    this.moreDropdown.close()
+  }
+
+  updateTimestamps (video: Video) {
+    const body: VideoPlaylistElementUpdate = {}
+
+    body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null
+    body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null
+
+    this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, video.id, body)
+        .subscribe(
+          () => {
+            this.notifier.success(this.i18n('Timestamps updated'))
+
+            video.playlistElement.startTimestamp = body.startTimestamp
+            video.playlistElement.stopTimestamp = body.stopTimestamp
+          },
+
+          err => this.notifier.error(err.message)
+        )
+
+    this.moreDropdown.close()
+  }
+
+  formatTimestamp (video: Video) {
+    const start = video.playlistElement.startTimestamp
+    const stop = video.playlistElement.stopTimestamp
+
+    const startFormatted = secondsToTime(start, true, ':')
+    const stopFormatted = secondsToTime(stop, true, ':')
+
+    if (start === null && stop === null) return ''
+
+    if (start !== null && stop === null) return this.i18n('Starts at ') + startFormatted
+    if (start === null && stop !== null) return this.i18n('Stops at ') + stopFormatted
+
+    return this.i18n('Starts at ') + startFormatted + this.i18n(' and stops at ') + stopFormatted
+  }
+
+  onDropdownOpenChange () {
+    this.displayTimestampOptions = false
+  }
+
+  toggleDisplayTimestampsOptions (event: Event, video: Video) {
+    event.preventDefault()
+
+    this.displayTimestampOptions = !this.displayTimestampOptions
+
+    if (this.displayTimestampOptions === true) {
+      this.timestampOptions = {
+        startTimestampEnabled: false,
+        stopTimestampEnabled: false,
+        startTimestamp: 0,
+        stopTimestamp: video.duration
+      }
+
+      if (video.playlistElement.startTimestamp) {
+        this.timestampOptions.startTimestampEnabled = true
+        this.timestampOptions.startTimestamp = video.playlistElement.startTimestamp
+      }
+
+      if (video.playlistElement.stopTimestamp) {
+        this.timestampOptions.stopTimestampEnabled = true
+        this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp
+      }
+    }
+  }
+}
index a02e9444a72cd91f26d892d7a436b9534906b8e5..186597a3a04d9bd911a8e113a191b30704f0658b 100644 (file)
@@ -11,6 +11,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
   @Input() firstLoadedPage = 1
   @Input() percentLimit = 70
   @Input() autoInit = false
+  @Input() container = document.body
 
   @Output() nearOfBottom = new EventEmitter<void>()
   @Output() nearOfTop = new EventEmitter<void>()
@@ -48,7 +49,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
       .pipe(
         startWith(null),
         throttleTime(200, undefined, throttleOptions),
-        map(() => ({ current: window.scrollY, maximumScroll: document.body.clientHeight - window.innerHeight })),
+        map(() => ({ current: window.scrollY, maximumScroll: this.container.clientHeight - window.innerHeight })),
         distinctUntilChanged((o1, o2) => o1.current === o2.current),
         share()
       )
index a6757fc4a211c4cd6ef0c27fbb9092963ec2ed5f..b302ebd0f18921e2a636f9acd94b2a09a85b8083 100644 (file)
@@ -1,5 +1,5 @@
 <a
-  [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
+  [routerLink]="getVideoRouterLink()" [queryParams]="queryParams" [attr.title]="video.name"
   class="video-thumbnail"
 >
   <img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
index ca43700c7c7d620eba0ce29fe3034564569eb714..fe65ade9442a1b5da0beedaab65b269fa699d0fd 100644 (file)
@@ -10,8 +10,11 @@ import { ScreenService } from '@app/shared/misc/screen.service'
 export class VideoThumbnailComponent {
   @Input() video: Video
   @Input() nsfw = false
+  @Input() routerLink: any[]
+  @Input() queryParams: any[]
 
-  constructor (private screenService: ScreenService) {}
+  constructor (private screenService: ScreenService) {
+  }
 
   getImageUrl () {
     if (!this.video) return ''
@@ -30,4 +33,10 @@ export class VideoThumbnailComponent {
 
     return (currentTime / this.video.duration) * 100
   }
+
+  getVideoRouterLink () {
+    if (this.routerLink) return this.routerLink
+
+    return [ '/videos/watch', this.video.uuid ]
+  }
 }
index 0d78090445f2c1a802d574fb0e55c07db4c3b186..ce9250bdc756c8d80ba72a7e935c6e39372769b2 100644 (file)
@@ -7,16 +7,16 @@ import { VideoWatchComponent } from './video-watch.component'
 
 const videoWatchRoutes: Routes = [
   {
-    path: 'playlist/:uuid',
+    path: 'playlist/:playlistId',
     component: VideoWatchComponent,
     canActivate: [ MetaGuard ]
   },
   {
-    path: ':uuid/comments/:commentId',
-    redirectTo: ':uuid'
+    path: ':videoId/comments/:commentId',
+    redirectTo: ':videoId'
   },
   {
-    path: ':uuid',
+    path: ':videoId',
     component: VideoWatchComponent,
     canActivate: [ MetaGuard ]
   }
index 394c31f23fc830963b2ede2a98c6d9a9b59fa026..7f3d1cc2ed80ac331d120123a8d41a096dba0441 100644 (file)
@@ -1,11 +1,39 @@
 <div class="root-row row">
   <!-- We need the video container for videojs so we just hide it -->
-  <div id="video-element-wrapper">
+  <div id="video-wrapper">
     <div *ngIf="remoteServerDown" class="remote-server-down">
       Sorry, but this video is not available because the remote instance is not responding.
       <br />
       Please try again later.
     </div>
+
+    <div id="videojs-wrapper"></div>
+
+    <div *ngIf="playlist && video" class="playlist">
+      <div class="playlist-info">
+        <div class="playlist-display-name">
+          {{ playlist.displayName }}
+
+          <span *ngIf="isUnlistedPlaylist()" class="badge badge-warning" i18n>Unlisted</span>
+          <span *ngIf="isPrivatePlaylist()" class="badge badge-danger" i18n>Private</span>
+          <span *ngIf="isPublicPlaylist()" class="badge badge-info" i18n>Public</span>
+        </div>
+
+        <div class="playlist-by-index">
+          <div class="playlist-by">{{ playlist.ownerBy }}</div>
+          <div class="playlist-index">
+            <span>{{currentPlaylistPosition}}</span><span>{{playlistPagination.totalItems}}</span>
+          </div>
+        </div>
+      </div>
+
+      <div *ngFor="let playlistVideo of playlistVideos" myInfiniteScroller [autoInit]="true" #elem [container]="elem" (nearOfBottom)="onPlaylistVideosNearOfBottom()">
+        <my-video-playlist-element-miniature
+          [video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
+          [playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false"
+        ></my-video-playlist-element-miniature>
+      </div>
+    </div>
   </div>
 
   <div i18n class="alert alert-warning" *ngIf="isVideoToImport()">
     This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}.
   </div>
 
+  <div i18n class="alert alert-info" *ngIf="noPlaylistVideos">
+    This playlist does not have videos.
+  </div>
+
   <div class="alert alert-danger" *ngIf="video?.blacklisted">
     <div class="blacklisted-label" i18n>This video is blacklisted.</div>
     {{ video.blacklistedReason }}
index 44040e90d699c24c52f568a6dad60b7d9eb007ec..e1cb249efcf77c5c0b4bb041722adcd80ca75cab 100644 (file)
@@ -1,6 +1,7 @@
 @import '_variables';
 @import '_mixins';
 @import '_bootstrap-variables';
+@import '_miniature';
 
 $other-videos-width: 260px;
 
@@ -12,7 +13,7 @@ $other-videos-width: 260px;
   font-weight: $font-semibold;
 }
 
-#video-element-wrapper {
+#video-wrapper {
   background-color: #000;
   display: flex;
   justify-content: center;
@@ -39,6 +40,57 @@ $other-videos-width: 260px;
     }
   }
 
+  .playlist {
+    width: 400px;
+    height: 66vh;
+    background-color: #e4e4e4;
+    overflow-y: auto;
+
+    .playlist-info {
+      padding: 5px 30px;
+
+      .playlist-display-name {
+        font-size: 18px;
+        font-weight: $font-semibold;
+        margin-bottom: 5px;
+      }
+
+      .playlist-by-index {
+        color: $grey-foreground-color;
+        display: flex;
+
+        .playlist-by {
+          margin-right: 5px;
+        }
+
+        .playlist-index span:first-child::after {
+          content: '/';
+          margin: 0 3px;
+        }
+      }
+    }
+
+    my-video-playlist-element-miniature {
+      /deep/ {
+        .video {
+          .position {
+            margin-right: 0;
+          }
+
+          .video-info {
+            .video-info-name {
+              font-size: 15px;
+            }
+          }
+        }
+
+        my-video-thumbnail {
+          @include thumbnail-size-component(90px, 50px);
+        }
+      }
+    }
+  }
+
   /deep/ .video-js {
     width: calc(66vh * 1.77);
     height: 66vh;
index 359217f3b8d4ed714449c265d1ec6ae9f7aac216..ddd0f17661bb64c1e1488c38b928c36e2b5270d7 100644 (file)
@@ -8,7 +8,7 @@ import { MetaService } from '@ngx-meta/core'
 import { Notifier, ServerService } from '@app/core'
 import { forkJoin, Subscription } from 'rxjs'
 import { Hotkey, HotkeysService } from 'angular2-hotkeys'
-import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
+import { UserVideoRateType, VideoCaption, VideoPlaylistPrivacy, VideoPrivacy, VideoState } from '../../../../../shared'
 import { AuthService, ConfirmService } from '../../core'
 import { RestExtractor, VideoBlacklistService } from '../../shared'
 import { VideoDetails } from '../../shared/video/video-details.model'
@@ -28,6 +28,10 @@ import {
   PeertubePlayerManagerOptions,
   PlayerMode
 } from '../../../assets/player/peertube-player-manager'
+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'
 
 @Component({
   selector: 'my-video-watch',
@@ -50,6 +54,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   video: VideoDetails = null
   descriptionLoading = false
 
+  playlist: VideoPlaylist = null
+  playlistVideos: Video[] = []
+  playlistPagination: ComponentPagination = {
+    currentPage: 1,
+    itemsPerPage: 10,
+    totalItems: null
+  }
+  noPlaylistVideos = false
+  currentPlaylistPosition = 1
+
   completeDescriptionShown = false
   completeVideoDescription: string
   shortVideoDescription: string
@@ -61,6 +75,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
   private currentTime: number
   private paramsSub: Subscription
+  private queryParamsSub: Subscription
 
   constructor (
     private elementRef: ElementRef,
@@ -68,6 +83,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     private route: ActivatedRoute,
     private router: Router,
     private videoService: VideoService,
+    private playlistService: VideoPlaylistService,
     private videoBlacklistService: VideoBlacklistService,
     private confirmService: ConfirmService,
     private metaService: MetaService,
@@ -97,31 +113,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     }
 
     this.paramsSub = this.route.params.subscribe(routeParams => {
-      const uuid = routeParams[ 'uuid' ]
+      const videoId = routeParams[ 'videoId' ]
+      if (videoId) this.loadVideo(videoId)
 
-      // Video did not change
-      if (this.video && this.video.uuid === uuid) return
-
-      if (this.player) this.player.pause()
+      const playlistId = routeParams[ 'playlistId' ]
+      if (playlistId) this.loadPlaylist(playlistId)
+    })
 
-      // Video did change
-      forkJoin(
-        this.videoService.getVideo(uuid),
-        this.videoCaptionService.listCaptions(uuid)
-      )
-        .pipe(
-          // If 401, the video is private or blacklisted so redirect to 404
-          catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
-        )
-        .subscribe(([ video, captionsResult ]) => {
-          const startTime = this.route.snapshot.queryParams.start
-          const stopTime = this.route.snapshot.queryParams.stop
-          const subtitle = this.route.snapshot.queryParams.subtitle
-          const playerMode = this.route.snapshot.queryParams.mode
-
-          this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode })
-              .catch(err => this.handleError(err))
-        })
+    this.queryParamsSub = this.route.queryParams.subscribe(queryParams => {
+      const videoId = queryParams[ 'videoId' ]
+      if (videoId) this.loadVideo(videoId)
     })
 
     this.hotkeys = [
@@ -147,7 +148,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.flushPlayer()
 
     // Unsubscribe subscriptions
-    this.paramsSub.unsubscribe()
+    if (this.paramsSub) this.paramsSub.unsubscribe()
+    if (this.queryParamsSub) this.queryParamsSub.unsubscribe()
 
     // Unbind hotkeys
     if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys)
@@ -219,8 +221,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   }
 
   showShareModal () {
-    const currentTime = this.player ? this.player.currentTime() : undefined
-
     this.videoShareModal.show(this.currentTime)
   }
 
@@ -322,6 +322,107 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     return this.video && this.video.scheduledUpdate !== undefined
   }
 
+  isVideoBlur (video: Video) {
+    return video.isVideoNSFWForUser(this.user, this.serverService.getConfig())
+  }
+
+  isPlaylistOwned () {
+    return this.playlist.isLocal === true && this.playlist.ownerAccount.name === this.user.username
+  }
+
+  isUnlistedPlaylist () {
+    return this.playlist.privacy.id === VideoPlaylistPrivacy.UNLISTED
+  }
+
+  isPrivatePlaylist () {
+    return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE
+  }
+
+  isPublicPlaylist () {
+    return this.playlist.privacy.id === VideoPlaylistPrivacy.PUBLIC
+  }
+
+  onPlaylistVideosNearOfBottom () {
+    // Last page
+    if (this.playlistPagination.totalItems <= (this.playlistPagination.currentPage * this.playlistPagination.itemsPerPage)) return
+
+    this.playlistPagination.currentPage += 1
+    this.loadPlaylistElements(false)
+  }
+
+  onElementRemoved (video: Video) {
+    this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id)
+
+    this.playlistPagination.totalItems--
+  }
+
+  private loadVideo (videoId: string) {
+    // Video did not change
+    if (this.video && this.video.uuid === videoId) return
+
+    if (this.player) this.player.pause()
+
+    // Video did change
+    forkJoin(
+      this.videoService.getVideo(videoId),
+      this.videoCaptionService.listCaptions(videoId)
+    )
+      .pipe(
+        // If 401, the video is private or blacklisted so redirect to 404
+        catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
+      )
+      .subscribe(([ video, captionsResult ]) => {
+        const queryParams = this.route.snapshot.queryParams
+        const startTime = queryParams.start
+        const stopTime = queryParams.stop
+        const subtitle = queryParams.subtitle
+        const playerMode = queryParams.mode
+
+        this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode })
+            .catch(err => this.handleError(err))
+      })
+  }
+
+  private loadPlaylist (playlistId: string) {
+    // Playlist did not change
+    if (this.playlist && this.playlist.uuid === playlistId) return
+
+    this.playlistService.getVideoPlaylist(playlistId)
+      .pipe(
+        // If 401, the video is private or blacklisted so redirect to 404
+        catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
+      )
+      .subscribe(playlist => {
+        this.playlist = playlist
+
+        const videoId = this.route.snapshot.queryParams['videoId']
+        this.loadPlaylistElements(!videoId)
+      })
+  }
+
+  private loadPlaylistElements (redirectToFirst = false) {
+    this.videoService.getPlaylistVideos(this.playlist.id, this.playlistPagination)
+        .subscribe(({ totalVideos, videos }) => {
+          this.playlistVideos = this.playlistVideos.concat(videos)
+          this.playlistPagination.totalItems = totalVideos
+
+          if (totalVideos === 0) {
+            this.noPlaylistVideos = true
+            return
+          }
+
+          this.updatePlaylistIndex()
+
+          if (redirectToFirst) {
+            const extras = {
+              queryParams: { videoId: this.playlistVideos[ 0 ].uuid },
+              replaceUrl: true
+            }
+            this.router.navigate([], extras)
+          }
+        })
+  }
+
   private updateVideoDescription (description: string) {
     this.video.description = description
     this.setVideoDescriptionHTML()
@@ -383,11 +484,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.remoteServerDown = false
     this.currentTime = undefined
 
+    this.updatePlaylistIndex()
+
     let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0)
     // If we are at the end of the video, reset the timer
     if (this.video.duration - startTime <= 1) startTime = 0
 
-    if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
+    if (this.isVideoBlur(this.video)) {
       const res = await this.confirmService.confirm(
         this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'),
         this.i18n('Mature or explicit content')
@@ -399,7 +502,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.flushPlayer()
 
     // Build video element, because videojs remove it on dispose
-    const playerElementWrapper = this.elementRef.nativeElement.querySelector('#video-element-wrapper')
+    const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper')
     this.playerElement = document.createElement('video')
     this.playerElement.className = 'video-js vjs-peertube-skin'
     this.playerElement.setAttribute('playsinline', 'true')
@@ -474,6 +577,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
       this.player.on('timeupdate', () => {
         this.currentTime = Math.floor(this.player.currentTime())
       })
+
+      this.player.one('ended', () => {
+        if (this.playlist) {
+          this.zone.run(() => this.navigateToNextPlaylistVideo())
+        }
+      })
+
+      this.player.one('stopped', () => {
+        if (this.playlist) {
+          this.zone.run(() => this.navigateToNextPlaylistVideo())
+        }
+      })
     })
 
     this.setVideoDescriptionHTML()
@@ -528,6 +643,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.setVideoLikesBarTooltipText()
   }
 
+  private updatePlaylistIndex () {
+    if (this.playlistVideos.length === 0 || !this.video) return
+
+    for (const video of this.playlistVideos) {
+      if (video.id === this.video.id) {
+        this.currentPlaylistPosition = video.playlistElement.position
+        return
+      }
+    }
+
+    // Load more videos to find our video
+    this.onPlaylistVideosNearOfBottom()
+  }
+
   private setOpenGraphTags () {
     this.metaService.setTitle(this.video.name)
 
@@ -567,4 +696,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
       this.player = undefined
     }
   }
+
+  private navigateToNextPlaylistVideo () {
+    if (this.currentPlaylistPosition < this.playlistPagination.totalItems) {
+      const next = this.playlistVideos.find(v => v.playlistElement.position === this.currentPlaylistPosition + 1)
+
+      const start = next.playlistElement.startTimestamp
+      const stop = next.playlistElement.stopTimestamp
+      this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } })
+    }
+  }
 }
diff --git a/client/src/assets/images/global/play.html b/client/src/assets/images/global/play.html
new file mode 100644 (file)
index 0000000..d00122d
--- /dev/null
@@ -0,0 +1,9 @@
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
+    <g id="Artboard-4" transform="translate(-532.000000, -115.000000)" stroke-width="2" stroke="#000000">
+      <g id="12" transform="translate(532.000000, 115.000000)">
+        <polygon id="Triangle-1" points="5 21 5 3 21 12" fill="#000000"/>
+      </g>
+    </g>
+  </g>
+</svg>
index 4dbfda30058cf147d82d7aef0c00d4b44b50a586..bbd3e008d32cf09ee3538aa2cac6525b177a7883 100644 (file)
@@ -4,6 +4,7 @@ import * as videojs from 'video.js'
 import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings'
 import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
 import { Events } from 'p2p-media-loader-core'
+import { timeToInt } from '../utils'
 
 // videojs-hlsjs-plugin needs videojs in window
 window['videojs'] = videojs
@@ -32,6 +33,7 @@ class P2pMediaLoaderPlugin extends Plugin {
     totalDownload: 0,
     totalUpload: 0
   }
+  private startTime: number
 
   private networkInfoInterval: any
 
@@ -54,12 +56,14 @@ class P2pMediaLoaderPlugin extends Plugin {
 
     initVideoJsContribHlsJsPlayer(player)
 
+    this.startTime = timeToInt(options.startTime)
+
     player.src({
       type: options.type,
       src: options.src
     })
 
-    player.on('play', () => {
+    player.one('play', () => {
       player.addClass('vjs-has-big-play-button-clicked')
     })
 
@@ -92,6 +96,12 @@ class P2pMediaLoaderPlugin extends Plugin {
     this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length
 
     this.runStats()
+
+    this.hlsjs.on('hlsLevelLoaded', () => {
+      if (this.startTime) this.player.currentTime(this.startTime)
+
+      this.hlsjs.off('hlsLevelLoaded', this)
+    })
   }
 
   private runStats () {
index 3991e462712455c8699c67078b5173993d6cc46d..dd9408c8e280b37df8803cc3b9d96ddb36af24ef 100644 (file)
@@ -83,9 +83,15 @@ class PeerTubePlugin extends Plugin {
 
       if (options.stopTime) {
         const stopTime = timeToInt(options.stopTime)
+        const self = this
 
-        this.player.on('timeupdate', () => {
-          if (this.player.currentTime() > stopTime) this.player.pause()
+        this.player.on('timeupdate', function onTimeUpdate () {
+          if (self.player.currentTime() > stopTime) {
+            self.player.pause()
+            self.player.trigger('stopped')
+
+            self.player.off('timeupdate', onTimeUpdate)
+          }
         })
       }
 
index 25a024aaca2a007a2cd9926e2a681374f1439c2d..95b7592251315edd6938fef559f2f5738f3f460b 100644 (file)
@@ -28,15 +28,15 @@ $play-overlay-transition: 0.2s ease;
 $play-overlay-height: 26px;
 $play-overlay-width: 18px;
 
-@mixin miniature-thumbnail($width: $video-thumbnail-width, $height: $video-thumbnail-height) {
+@mixin miniature-thumbnail {
   @include disable-outline;
 
   display: inline-block;
   position: relative;
   border-radius: 3px;
   overflow: hidden;
-  width: $width;
-  height: $height;
+  width: $video-thumbnail-width;
+  height: $video-thumbnail-height;
   background-color: #ececec;
   transition: filter $play-overlay-transition;
 
@@ -97,6 +97,13 @@ $play-overlay-width: 18px;
   }
 }
 
+@mixin thumbnail-size-component ($width, $height) {
+  /deep/ .video-thumbnail {
+    width: $width;
+    height: $height;
+  }
+}
+
 @mixin static-thumbnail-overlay {
   display: inline-block;
   background-color: rgba(0, 0, 0, 0.7);
index 7faeec6bdcc0893e370b9f38a0d5cfe4b7b67420..9b18f63545df1ab56af7a5947d631183250bf4e6 100644 (file)
 
 @mixin apply-svg-color ($color) {
   /deep/ svg {
-    path[fill="#000000"], g[fill="#000000"], rect[fill="#000000"], circle[fill="#000000"] {
+    path[fill="#000000"], g[fill="#000000"], rect[fill="#000000"], circle[fill="#000000"], polygon[fill="#000000"] {
       fill: $color;
     }
 
-    path[stroke="#000000"], g[stroke="#000000"], rect[stroke="#000000"], circle[stroke="#000000"] {
+    path[stroke="#000000"], g[stroke="#000000"], rect[stroke="#000000"], circle[stroke="#000000"], polygon[stroke="#000000"] {
       stroke: $color;
     }
   }