]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Playlist reorder support
authorChocobozzz <me@florianbigard.com>
Tue, 12 Mar 2019 10:40:42 +0000 (11:40 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Mon, 18 Mar 2019 10:17:59 +0000 (11:17 +0100)
client/package.json
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/+my-account/my-account.module.ts
client/src/app/shared/shared.module.ts
client/src/app/shared/video-playlist/video-playlist.service.ts
client/yarn.lock
server/controllers/api/video-playlist.ts
server/models/video/video.ts
shared/models/videos/playlist/video-playlist-reorder.model.ts [new file with mode: 0644]

index 08d309b07b51d18c4531eda5419057a83bee7afc..72708bd7653d2e71ad55da95f471f3073285289a 100644 (file)
@@ -66,6 +66,7 @@
   "devDependencies": {
     "@angular-devkit/build-angular": "~0.13.1",
     "@angular/animations": "~7.2.4",
+    "@angular/cdk": "^7.3.4",
     "@angular/cli": "~7.3.1",
     "@angular/common": "~7.2.4",
     "@angular/compiler": "~7.2.4",
index e2d09a36d575ea16732a471d88bf4abf8bc0d4de..67a8b1a9169a3b11285c7b0250ba20658eb812aa 100644 (file)
@@ -1,7 +1,10 @@
 <div i18n class="no-results" *ngIf="pagination.totalItems === 0">No videos in this playlist.</div>
 
-<div class="videos" myInfiniteScroller (nearOfBottom)="onNearOfBottom()">
-  <div *ngFor="let video of videos" class="video">
+<div
+  class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"
+  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>
index 3be10078e217cc129132a614d30c522c5d1867d6..4ac89d08f4ebb74d18c1e1090c1fc8aaf410ab1a 100644 (file)
 @import '_mixins';
 @import '_miniature';
 
-.videos {
-  .video {
+.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;
-    align-items: center;
-    padding: 10px;
-    border-bottom: 1px solid $separator-border-color;
+    flex-direction: column;
 
-    &:hover {
-      background-color: rgba(0, 0, 0, 0.05);
+    a {
+      @include disable-default-a-behaviour;
 
-      .more {
-        display: block;
-      }
+      color: var(--mainForegroundColor);
     }
 
-    .position {
+    .video-info-name {
+      font-size: 18px;
       font-weight: $font-semibold;
-      margin-right: 10px;
-      color: $grey-foreground-color;
     }
 
-    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-account, .video-info-timestamp {
+      color: $grey-foreground-color;
     }
+  }
 
-    .video-info {
-      display: flex;
-      flex-direction: column;
+  .more {
+    justify-self: flex-end;
+    margin-left: auto;
+    cursor: pointer;
+    display: none;
 
-      a {
-        @include disable-default-a-behaviour;
+    &.show {
+      display: block;
+    }
 
-        color: var(--mainForegroundColor);
-      }
+    .icon-more {
+      @include apply-svg-color($grey-foreground-color);
 
-      .video-info-name {
-        font-size: 18px;
-        font-weight: $font-semibold;
+      &::after {
+        border: none;
       }
+    }
 
-      .video-info-account, .video-info-timestamp {
-        color: $grey-foreground-color;
-      }
+    .dropdown-item {
+      @include dropdown-with-icon-item;
     }
 
-    .more {
-      justify-self: flex-end;
-      margin-left: auto;
-      cursor: pointer;
-      display: none;
+    .timestamp-options {
+      padding-top: 0;
+      padding-left: 35px;
+      margin-bottom: 15px;
 
-      &.show {
-        display: block;
+      > div {
+        display: flex;
+        align-items: center;
       }
 
-      .icon-more {
-        @include apply-svg-color($grey-foreground-color);
+      input {
+        @include peertube-button;
+        @include orange-button;
 
-        &::after {
-          border: none;
-        }
+        margin-top: 10px;
       }
+    }
+  }
+}
 
-      .dropdown-item {
-        @include dropdown-with-icon-item;
-      }
+// Thanks Angular CDK <3 https://material.angular.io/cdk/drag-drop/examples
+.cdk-drag-preview {
+  box-sizing: border-box;
+  border-radius: 4px;
+  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
+  0 8px 10px 1px rgba(0, 0, 0, 0.14),
+  0 3px 14px 2px rgba(0, 0, 0, 0.12);
+}
 
-      .timestamp-options {
-        padding-top: 0;
-        padding-left: 35px;
-        margin-bottom: 15px;
+.cdk-drag-placeholder {
+  opacity: 0;
+}
 
-        > div {
-          display: flex;
-          align-items: center;
-        }
+.cdk-drag-animating {
+  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
+}
 
-        input {
-          @include peertube-button;
-          @include orange-button;
+.video:last-child {
+  border: none;
+}
 
-          margin-top: 10px;
-        }
-      }
-    }
-  }
+.videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) {
+  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
 }
index 76aff3d4fc0c3aab8d4f9e772924dfe1daeb4323..4076a3721c30a50b542182104feadcbcd029a820 100644 (file)
@@ -4,7 +4,7 @@ import { AuthService } from '../../core/auth'
 import { ConfirmService } from '../../core/confirm'
 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
 import { Video } from '@app/shared/video/video.model'
-import { Subscription } from 'rxjs'
+import { Subject, Subscription } from 'rxjs'
 import { ActivatedRoute } from '@angular/router'
 import { VideoService } from '@app/shared/video/video.service'
 import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
@@ -13,6 +13,8 @@ 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'
 
 @Component({
   selector: 'my-account-video-playlist-elements',
@@ -42,6 +44,7 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
 
   private videoPlaylistId: string | number
   private paramsSub: Subscription
+  private dragMoveSubject = new Subject<number>()
 
   constructor (
     private authService: AuthService,
@@ -61,12 +64,66 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
 
       this.loadPlaylistInfo()
     })
+
+    this.dragMoveSubject.asObservable()
+      .pipe(throttleTime(200))
+      .subscribe(y => this.checkScroll(y))
   }
 
   ngOnDestroy () {
     if (this.paramsSub) this.paramsSub.unsubscribe()
   }
 
+  drop (event: CdkDragDrop<any>) {
+    const previousIndex = event.previousIndex
+    const newIndex = event.currentIndex
+
+    if (previousIndex === newIndex) return
+
+    const oldPosition = this.videos[previousIndex].playlistElement.position
+    const insertAfter = newIndex === 0 ? 0 : this.videos[newIndex].playlistElement.position
+
+    this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter)
+      .subscribe(
+        () => { /* nothing to do */ },
+
+        err => this.notifier.error(err.message)
+      )
+
+    const video = this.videos[previousIndex]
+
+    this.videos.splice(previousIndex, 1)
+    this.videos.splice(newIndex, 0, video)
+
+    this.reorderClientPositions()
+  }
+
+  onDragMove (event: CdkDragMove<any>) {
+    this.dragMoveSubject.next(event.pointerPosition.y)
+  }
+
+  checkScroll (pointerY: number) {
+    // FIXME: Uncomment when https://github.com/angular/material2/issues/14098 is fixed
+    // FIXME: Remove when https://github.com/angular/material2/issues/13588 is implemented
+    // if (pointerY < 150) {
+    //   window.scrollBy({
+    //     left: 0,
+    //     top: -20,
+    //     behavior: 'smooth'
+    //   })
+    //
+    //   return
+    // }
+    //
+    // if (window.innerHeight - pointerY <= 50) {
+    //   window.scrollBy({
+    //     left: 0,
+    //     top: 20,
+    //     behavior: 'smooth'
+    //   })
+    // }
+  }
+
   isVideoBlur (video: Video) {
     return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
   }
@@ -78,6 +135,7 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
             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)
@@ -173,4 +231,13 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
         this.playlist = playlist
       })
   }
+
+  private reorderClientPositions () {
+    let i = 1
+
+    for (const video of this.videos) {
+      video.playlistElement.position = i
+      i++
+    }
+  }
 }
index ba83001119ae398f56e7c628d95633b2b06d5698..4a18a9968103f8574732fe621fb52eb91c1095f0 100644 (file)
@@ -35,6 +35,7 @@ import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-vi
 import {
   MyAccountVideoPlaylistElementsComponent
 } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
+import { DragDropModule } from '@angular/cdk/drag-drop'
 
 @NgModule({
   imports: [
@@ -43,7 +44,8 @@ import {
     AutoCompleteModule,
     SharedModule,
     TableModule,
-    InputSwitchModule
+    InputSwitchModule,
+    DragDropModule
   ],
 
   declarations: [
index 1f9eee0b7ec84fc12800b761056c77b95600916c..05da0d82926f4a68a1cd088b764c04cb80b48bda 100644 (file)
@@ -9,7 +9,6 @@ import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.d
 
 import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
 import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
-import { KeyFilterModule } from 'primeng/keyfilter'
 
 import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
 import { ButtonComponent } from './buttons/button.component'
@@ -95,7 +94,6 @@ import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.compo
 
     PrimeSharedModule,
     InputMaskModule,
-    KeyFilterModule,
     NgPipesModule
   ],
 
@@ -155,7 +153,6 @@ import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.compo
 
     PrimeSharedModule,
     InputMaskModule,
-    KeyFilterModule,
     BytesPipe,
     KeysPipe,
 
index f7b37f83aabc4491bb7d1cad9da2f2746ae8dd7e..da7437507b5c048dc780c2b63c78ebe5243cf8c1 100644 (file)
@@ -17,6 +17,7 @@ import { AccountService } from '@app/shared/account/account.service'
 import { Account } from '@app/shared/account/account.model'
 import { RestService } from '@app/shared/rest'
 import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model'
+import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model'
 
 @Injectable()
 export class VideoPlaylistService {
@@ -125,6 +126,19 @@ export class VideoPlaylistService {
                )
   }
 
+  reorderPlaylist (playlistId: number, oldPosition: number, newPosition: number) {
+    const body: VideoPlaylistReorder = {
+      startPosition: oldPosition,
+      insertAfterPosition: newPosition
+    }
+
+    return this.authHttp.post(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/reorder', body)
+               .pipe(
+                 map(this.restExtractor.extractDataBool),
+                 catchError(err => this.restExtractor.handleError(err))
+               )
+  }
+
   doesVideoExistInPlaylist (videoId: number) {
     this.videoExistsInPlaylistSubject.next(videoId)
 
index 2d3ade3dd436a80a7de34a82cf648beab2e279f3..8f643aad472da4a78060229a52a7280c8e415190 100644 (file)
   dependencies:
     tslib "^1.9.0"
 
+"@angular/cdk@^7.3.4":
+  version "7.3.4"
+  resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-7.3.4.tgz#b9d8c62cdd24fa6517dd3ae68c78632b1525d35f"
+  integrity sha512-cHl1o7obogCO3Nxf9n8MrXpfHa7AH1QNX2BY+bftYBTHW++YJe+qAwkwWLVqnJD9TQE2OpiR058zoJU20khM/g==
+  dependencies:
+    tslib "^1.7.1"
+  optionalDependencies:
+    parse5 "^5.0.0"
+
 "@angular/cli@~7.3.1":
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-7.3.1.tgz#a18acdec84deb03a1fae79cae415bbc8f9c87ffa"
@@ -7469,6 +7478,11 @@ parse5@4.0.0:
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
   integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
 
+parse5@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
+  integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
+
 parseqs@0.0.5:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
index 49432d3aa92974291bea6b888a11ec0cc291660d..0a7ff92df21218490556173b825a7726ead4b4bd 100644 (file)
@@ -41,6 +41,7 @@ import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playli
 import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
 import { copy, pathExists } from 'fs-extra'
 import { AccountModel } from '../../models/account/account'
+import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model'
 
 const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
 
@@ -368,10 +369,11 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo
 
 async function reorderVideosPlaylist (req: express.Request, res: express.Response) {
   const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist
+  const body: VideoPlaylistReorder = req.body
 
-  const start: number = req.body.startPosition
-  const insertAfter: number = req.body.insertAfterPosition
-  const reorderLength: number = req.body.reorderLength || 1
+  const start: number = body.startPosition
+  const insertAfter: number = body.insertAfterPosition
+  const reorderLength: number = body.reorderLength || 1
 
   if (start === insertAfter) {
     return res.status(204).end()
index 06c63e87c42a0a372e3c372230d9f4bdde1e98b8..7624b064904c63e83c382ab6e487aec776fe727f 100644 (file)
@@ -240,7 +240,10 @@ type AvailableForListIDsOptions = {
     if (options.videoPlaylistId) {
       query.include.push({
         model: VideoPlaylistElementModel.unscoped(),
-        required: true
+        required: true,
+        where: {
+          videoPlaylistId: options.videoPlaylistId
+        }
       })
     }
 
@@ -304,6 +307,8 @@ type AvailableForListIDsOptions = {
           videoPlaylistId: options.videoPlaylistId
         }
       })
+
+      query.subQuery = false
     }
 
     if (options.filter || options.accountId || options.videoChannelId) {
diff --git a/shared/models/videos/playlist/video-playlist-reorder.model.ts b/shared/models/videos/playlist/video-playlist-reorder.model.ts
new file mode 100644 (file)
index 0000000..63ec714
--- /dev/null
@@ -0,0 +1,5 @@
+export interface VideoPlaylistReorder {
+  startPosition: number
+  insertAfterPosition: number
+  reorderLength?: number
+}