Playlist reorder support
This commit is contained in:
parent
c5a1ae500e
commit
15e9d5ca39
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface VideoPlaylistReorder {
|
||||
startPosition: number
|
||||
insertAfterPosition: number
|
||||
reorderLength?: number
|
||||
}
|
Loading…
Reference in New Issue