From 223b24e618146f85b20b5bf365bc18d14a5964cd Mon Sep 17 00:00:00 2001
From: Rigel Kent <sendmemail@rigelk.eu>
Date: Fri, 20 Dec 2019 17:49:57 +0100
Subject: [PATCH] Fix upnext, refactor avatar menu, add to playlist overflow

---
 .../src/app/+accounts/accounts.component.html |  2 +-
 .../menu/avatar-notification.component.html   |  2 +-
 .../menu/avatar-notification.component.scss   |  2 +-
 client/src/app/menu/menu.component.html       | 18 +++++--
 client/src/app/menu/menu.component.scss       |  1 +
 client/src/app/shared/misc/utils.ts           | 24 ++++++++-
 .../video-add-to-playlist.component.html      | 14 ++---
 .../video-add-to-playlist.component.scss      | 10 ++--
 .../+video-watch/video-watch.component.html   |  4 +-
 .../+video-watch/video-watch.component.scss   |  6 +--
 .../+video-watch/video-watch.component.ts     | 16 +++++-
 .../src/assets/player/upnext/upnext-plugin.ts | 54 ++++++++++---------
 client/src/sass/player/upnext.scss            |  6 +++
 13 files changed, 108 insertions(+), 51 deletions(-)

diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 6269091df6..70257162d5 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -23,7 +23,7 @@
           <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
 
           <my-user-moderation-dropdown
-            buttonSize="small" [account]="account" [user]="user"
+            buttonSize="small" [account]="account" [user]="user" placement="bottom-right auto"
             (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
           >
           </my-user-moderation-dropdown>
diff --git a/client/src/app/menu/avatar-notification.component.html b/client/src/app/menu/avatar-notification.component.html
index 1b6e6dcf8b..8ffec46da6 100644
--- a/client/src/app/menu/avatar-notification.component.html
+++ b/client/src/app/menu/avatar-notification.component.html
@@ -25,7 +25,7 @@
       </div>
     </div>
 
-    <div *ngIf="!loaded" class="loader">
+    <div *ngIf="!loaded" class="loader mt-4">
       <my-loader [loading]="!loaded"></my-loader>
     </div>
 
diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/avatar-notification.component.scss
index 713ac7cb91..2ca7f24dca 100644
--- a/client/src/app/menu/avatar-notification.component.scss
+++ b/client/src/app/menu/avatar-notification.component.scss
@@ -45,7 +45,7 @@
         align-items: center;
         padding: 0 10px;
         font-size: 16px;
-        height: 50px;
+        min-height: 50px;
 
         a {
           @include disable-default-a-behaviour;
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 3f406586e4..848f9949f9 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -5,8 +5,10 @@
         <my-avatar-notification [user]="user"></my-avatar-notification>
 
         <div class="logged-in-info">
-          <a routerLink="/my-account/settings" class="logged-in-display-name">{{ user.account?.displayName }}</a>
-          <div class="logged-in-username">{{ user.username }}</div>
+          <a *ngIf="user.account" [routerLink]="[ '/accounts', user.account.nameWithHost ]" class="logged-in-display-name">{{ user.account?.displayName }}</a>
+          <a *ngIf="!user.account" routerLink="/my-account/settings" class="logged-in-display-name">{{ user.account?.displayName }}</a>
+
+          <div ngxClipboard [cbContent]="user.account?.nameWithHost" class="logged-in-username">{{ user.username }}</div>
         </div>
 
         <div class="logged-in-more" ngbDropdown placement="bottom-right auto">
@@ -14,13 +16,21 @@
 
           <div ngbDropdownMenu>
             <a *ngIf="user.account" [routerLink]="[ '/accounts', user.account.nameWithHost ]" class="dropdown-item">
-              <my-global-icon iconName="go"></my-global-icon> <ng-container i18n>My public profile</ng-container>
+              <my-global-icon iconName="go"></my-global-icon> <ng-container i18n>Public profile</ng-container>
             </a>
 
+            <div class="dropdown-divider"></div>
+
             <a routerLink="/my-account" class="dropdown-item">
-              <my-global-icon iconName="user"></my-global-icon> <ng-container i18n>My account</ng-container>
+              <my-global-icon iconName="user"></my-global-icon> <ng-container i18n>Account settings</ng-container>
             </a>
 
+            <a routerLink="/my-account/video-channels" class="dropdown-item">
+              <my-global-icon iconName="folder"></my-global-icon> <ng-container i18n>Channels settings</ng-container>
+            </a>
+
+            <div class="dropdown-divider"></div>
+
             <a (click)="logout($event)" class="dropdown-item" href="#">
               <my-global-icon iconName="sign-out"></my-global-icon> <ng-container i18n>Log out</ng-container>
             </a>
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index 79a28d258f..2963d4d191 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -69,6 +69,7 @@ menu {
         font-size: 13px;
         color: #C6C6C6;
         max-width: 140px;
+        cursor: pointer;
       }
     }
 
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index f26240d216..b1d1fc0b52 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -169,6 +169,26 @@ function importModule (path: string) {
   })
 }
 
+function isInViewport (el: HTMLElement) {
+  const bounding = el.getBoundingClientRect()
+  return (
+      bounding.top >= 0 &&
+      bounding.left >= 0 &&
+      bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+      bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
+  )
+}
+
+function isXPercentInViewport (el: HTMLElement, percentVisible: number) {
+  const rect = el.getBoundingClientRect()
+  const windowHeight = (window.innerHeight || document.documentElement.clientHeight)
+
+  return !(
+    Math.floor(100 - (((rect.top >= 0 ? 0 : rect.top) / +-(rect.height / 1)) * 100)) < percentVisible ||
+    Math.floor(100 - ((rect.bottom - windowHeight) / rect.height) * 100) < percentVisible
+  )
+}
+
 export {
   sortBy,
   durationToString,
@@ -183,5 +203,7 @@ export {
   objectLineFeedToHtml,
   removeElementFromArray,
   importModule,
-  scrollToTop
+  scrollToTop,
+  isInViewport,
+  isXPercentInViewport
 }
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
index 648d580fa3..0cc8af3452 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
@@ -41,14 +41,16 @@
     </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" [onPushWorkaround]="true"></my-peertube-checkbox>
+  <div class="playlists">
+    <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" [onPushWorkaround]="true"></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>
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss b/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
index c677fea6ce..090b530cf1 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
@@ -1,11 +1,6 @@
 @import '_variables';
 @import '_mixins';
 
-.root {
-  max-height: 300px;
-  overflow-y: auto;
-}
-
 .header {
   min-width: 240px;
   padding: 6px 24px 10px 24px;
@@ -53,6 +48,11 @@
   padding: 6px 24px;
 }
 
+.playlists {
+  max-height: 180px;
+  overflow-y: auto;
+}
+
 .playlist {
   display: flex;
   cursor: pointer;
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index 7a9f00a505..9e69033e18 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -114,9 +114,9 @@
                   ></my-video-actions-dropdown>
                 </div>
 
-                <div class="video-info-likes-dislikes-bar-outerContainer">
+                <div class="video-info-likes-dislikes-bar-outer-container">
                   <div
-                    class="video-info-likes-dislikes-bar-innerContainer"
+                    class="video-info-likes-dislikes-bar-inner-container"
                     *ngIf="video.likes !== 0 || video.dislikes !== 0"
                     [ngbTooltip]="likesBarTooltipText"
                     placement="bottom"
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index 180b7c6ad2..f9ff83c342 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -297,13 +297,13 @@ $video-info-margin-left: 44px;
           }
         }
 
-        .video-info-likes-dislikes-bar-outerContainer {
+        .video-info-likes-dislikes-bar-outer-container {
           position: relative;
         }
 
-        .video-info-likes-dislikes-bar-innerContainer {
+        .video-info-likes-dislikes-bar-inner-container {
           position: absolute;
-          height: 30px;
+          height: 20px;
         }
 
         .video-info-likes-dislikes-bar {
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index bad0144bf8..dcceb14008 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -37,7 +37,7 @@ import { PluginService } from '@app/core/plugins/plugin.service'
 import { HooksService } from '@app/core/plugins/hooks.service'
 import { PlatformLocation } from '@angular/common'
 import { RecommendedVideosComponent } from '../recommendations/recommended-videos.component'
-import { scrollToTop } from '@app/shared/misc/utils'
+import { scrollToTop, isInViewport, isXPercentInViewport } from '@app/shared/misc/utils'
 
 @Component({
   selector: 'my-video-watch',
@@ -478,12 +478,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
       /**
        * replaces this.player.one('ended')
-       * define 'condition(next)' to return true to wait, false to stop
+       * 'condition()': true to make the upnext functionality trigger,
+       *                false to disable the upnext functionality
+       * go to the next video in 'condition()' if you don't want of the timer.
+       * 'next': function triggered at the end of the timer.
+       * 'suspended': function used at each clic of the timer checking if we need
+       * to reset progress and wait until 'suspended' becomes truthy again.
        */
       this.player.upnext({
         timeout: 10000, // 10s
         headText: this.i18n('Up Next'),
         cancelText: this.i18n('Cancel'),
+        suspendedText: this.i18n('Autoplay is suspended'),
         getTitle: () => this.nextVideoTitle,
         next: () => this.zone.run(() => this.autoplayNext()),
         condition: () => {
@@ -496,6 +502,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
             return true // upnext will trigger
           }
           return false // upnext will not trigger, and instead leave the video stopping
+        },
+        suspended: () => {
+          return (
+            !isXPercentInViewport(this.player.el(), 80) ||
+            !document.getElementById('content').contains(document.activeElement)
+          )
         }
       })
 
diff --git a/client/src/assets/player/upnext/upnext-plugin.ts b/client/src/assets/player/upnext/upnext-plugin.ts
index ba9afbe3de..a3747b25fa 100644
--- a/client/src/assets/player/upnext/upnext-plugin.ts
+++ b/client/src/assets/player/upnext/upnext-plugin.ts
@@ -18,6 +18,7 @@ function getMainTemplate (options: any) {
       <span class="vjs-upnext-cancel">
         <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button>
       </span>
+      <span class="vjs-upnext-suspended">${options.suspendedText}</span>
     </span>
   `
 }
@@ -26,40 +27,34 @@ function getMainTemplate (options: any) {
 const Component = videojs.getComponent('Component')
 class EndCard extends Component {
   options_: any
-  getTitle: Function
-  next: Function
-  condition: Function
   dashOffsetTotal = 586
   dashOffsetStart = 293
   interval = 50
   upNextEvents = new videojs.EventTarget()
-  chunkSize: number
+  ticks = 0
+  totalTicks: number
 
   container: HTMLElement
   title: HTMLElement
   autoplayRing: HTMLElement
   cancelButton: HTMLElement
+  suspendedMessage: HTMLElement
   nextButton: HTMLElement
 
   constructor (player: videojs.Player, options: any) {
     super(player, options)
-    this.options_ = options
 
-    this.getTitle = this.options_.getTitle
-    this.next = this.options_.next
-    this.condition = this.options_.condition
-
-    this.chunkSize = (this.dashOffsetTotal - this.dashOffsetStart) / (this.options_.timeout / this.interval)
+    this.totalTicks = this.options_.timeout / this.interval
 
     player.on('ended', (_: any) => {
-      if (!this.condition()) return
+      if (!this.options_.condition()) return
 
       player.addClass('vjs-upnext--showing')
       this.showCard((canceled: boolean) => {
         player.removeClass('vjs-upnext--showing')
         this.container.style.display = 'none'
         if (!canceled) {
-          this.next()
+          this.options_.next()
         }
       })
     })
@@ -81,6 +76,7 @@ class EndCard extends Component {
     this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0]
     this.title = container.getElementsByClassName('vjs-upnext-title')[0]
     this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0]
+    this.suspendedMessage = container.getElementsByClassName('vjs-upnext-suspended')[0]
     this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0]
 
     this.cancelButton.onclick = () => {
@@ -96,14 +92,11 @@ class EndCard extends Component {
 
   showCard (cb: Function) {
     let timeout: any
-    let start: number
-    let now: number
-    let newOffset: number
 
     this.autoplayRing.setAttribute('stroke-dasharray', '' + this.dashOffsetStart)
     this.autoplayRing.setAttribute('stroke-dashoffset', '' + -this.dashOffsetStart)
 
-    this.title.innerHTML = this.getTitle()
+    this.title.innerHTML = this.options_.getTitle()
 
     this.upNextEvents.one('cancel', () => {
       clearTimeout(timeout)
@@ -120,23 +113,32 @@ class EndCard extends Component {
       cb(false)
     })
 
-    const update = () => {
-      now = this.options_.timeout - (new Date().getTime() - start)
+    const goToPercent = (percent: number) => {
+      const newOffset = Math.max(-this.dashOffsetTotal, - this.dashOffsetStart - percent * this.dashOffsetTotal / 2 / 100)
+      this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset)
+    }
 
-      if (now <= 0) {
+    const tick = () => {
+      goToPercent((this.ticks++) * 100 / this.totalTicks)
+    }
+
+    const update = () => {
+      if (this.options_.suspended()) {
+        this.suspendedMessage.innerText = this.options_.suspendedText
+        goToPercent(0)
+        this.ticks = 0
+        timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer
+      } else if (this.ticks >= this.totalTicks) {
         clearTimeout(timeout)
         cb(false)
       } else {
-        const strokeDashOffset = parseInt(this.autoplayRing.getAttribute('stroke-dashoffset'), 10)
-        newOffset = Math.max(-this.dashOffsetTotal, strokeDashOffset - this.chunkSize)
-        this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset)
+        this.suspendedMessage.innerText = ''
+        tick()
         timeout = setTimeout(update.bind(this), this.interval)
       }
-
     }
 
     this.container.style.display = 'block'
-    start = new Date().getTime()
     timeout = setTimeout(update.bind(this), this.interval)
   }
 }
@@ -153,7 +155,9 @@ class UpNextPlugin extends Plugin {
       timeout: options.timeout || 5000,
       cancelText: options.cancelText || 'Cancel',
       headText: options.headText || 'Up Next',
-      condition: options.condition
+      suspendedText: options.suspendedText || 'Autoplay is suspended',
+      condition: options.condition,
+      suspended: options.suspended
     }
 
     super(player, settings)
diff --git a/client/src/sass/player/upnext.scss b/client/src/sass/player/upnext.scss
index f1f2e0fe24..7614bb3b67 100644
--- a/client/src/sass/player/upnext.scss
+++ b/client/src/sass/player/upnext.scss
@@ -40,12 +40,18 @@ $browser-context: 16;
     margin-top: 52px;
   }
 
+  .vjs-upnext-suspended,
   .vjs-upnext-cancel {
     display: block;
     float: none;
     text-align: center;
   }
 
+  .vjs-upnext-suspended {
+    font-size: 50%;
+    margin-top: 1rem;
+  }
+
   .vjs-upnext-headtext {
     display: block;
     font-size: 14px;
-- 
GitLab