Playlist reorder support

pull/1745/head
Chocobozzz 2019-03-12 11:40:42 +01:00 committed by Chocobozzz
parent c5a1ae500e
commit 15e9d5ca39
11 changed files with 228 additions and 92 deletions

View 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",

View 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>

View File

@ -2,95 +2,121 @@
@import '_mixins';
@import '_miniature';
.videos {
.video {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid $separator-border-color;
.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;
}
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;
}
}
&:hover {
background-color: rgba(0, 0, 0, 0.05);
.more {
justify-self: flex-end;
margin-left: auto;
cursor: pointer;
display: none;
display: block;
}
}
&.show {
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;
}
.icon-more {
@include apply-svg-color($grey-foreground-color);
input {
@include peertube-button;
@include orange-button;
&::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;
}
margin-top: 10px;
}
}
}
}
// 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);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.video:last-child {
border: none;
}
.videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

View 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++
}
}
}

View 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: [

View 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,

View 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)

View File

@ -107,6 +107,15 @@
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"

View 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()

View 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) {

View File

@ -0,0 +1,5 @@
export interface VideoPlaylistReorder {
startPosition: number
insertAfterPosition: number
reorderLength?: number
}