diff options
author | Chocobozzz <me@florianbigard.com> | 2019-03-12 11:40:42 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-03-18 11:17:59 +0100 |
commit | 15e9d5ca39e0b792f61453fbf3885a0fc446afa7 (patch) | |
tree | 015628bc7497f45477d287e8bb482e39d5d491e2 /client/src/app | |
parent | c5a1ae500e68b759f76851552be6dd10631d34f4 (diff) | |
download | PeerTube-15e9d5ca39e0b792f61453fbf3885a0fc446afa7.tar.gz PeerTube-15e9d5ca39e0b792f61453fbf3885a0fc446afa7.tar.zst PeerTube-15e9d5ca39e0b792f61453fbf3885a0fc446afa7.zip |
Playlist reorder support
Diffstat (limited to 'client/src/app')
6 files changed, 180 insertions, 71 deletions
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html index e2d09a36d..67a8b1a91 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html | |||
@@ -1,7 +1,10 @@ | |||
1 | <div i18n class="no-results" *ngIf="pagination.totalItems === 0">No videos in this playlist.</div> | 1 | <div i18n class="no-results" *ngIf="pagination.totalItems === 0">No videos in this playlist.</div> |
2 | 2 | ||
3 | <div class="videos" myInfiniteScroller (nearOfBottom)="onNearOfBottom()"> | 3 | <div |
4 | <div *ngFor="let video of videos" class="video"> | 4 | class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" |
5 | cdkDropList (cdkDropListDropped)="drop($event)" | ||
6 | > | ||
7 | <div class="video" *ngFor="let video of videos" cdkDrag (cdkDragMoved)="onDragMove($event)"> | ||
5 | <div class="position">{{ video.playlistElement.position }}</div> | 8 | <div class="position">{{ video.playlistElement.position }}</div> |
6 | 9 | ||
7 | <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur(video)"></my-video-thumbnail> | 10 | <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur(video)"></my-video-thumbnail> |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss index 3be10078e..4ac89d08f 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss | |||
@@ -2,95 +2,121 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | .videos { | 5 | .video, .cdk-drag-preview { |
6 | .video { | 6 | display: flex; |
7 | align-items: center; | ||
8 | background-color: var(--mainBackgroundColor); | ||
9 | cursor: pointer; | ||
10 | padding: 10px; | ||
11 | border-bottom: 1px solid $separator-border-color; | ||
12 | |||
13 | &:hover { | ||
14 | background-color: rgba(0, 0, 0, 0.05); | ||
15 | |||
16 | .more { | ||
17 | display: block; | ||
18 | } | ||
19 | } | ||
20 | |||
21 | .position { | ||
22 | font-weight: $font-semibold; | ||
23 | margin-right: 10px; | ||
24 | color: $grey-foreground-color; | ||
25 | min-width: 20px; | ||
26 | } | ||
27 | |||
28 | my-video-thumbnail { | ||
29 | display: flex; // Avoids an issue with line-height that adds space below the element | ||
30 | margin-right: 10px; | ||
31 | |||
32 | /deep/ .video-thumbnail { | ||
33 | @include miniature-thumbnail(130px, 72px); | ||
34 | } | ||
35 | } | ||
36 | |||
37 | .video-info { | ||
7 | display: flex; | 38 | display: flex; |
8 | align-items: center; | 39 | flex-direction: column; |
9 | padding: 10px; | ||
10 | border-bottom: 1px solid $separator-border-color; | ||
11 | 40 | ||
12 | &:hover { | 41 | a { |
13 | background-color: rgba(0, 0, 0, 0.05); | 42 | @include disable-default-a-behaviour; |
14 | 43 | ||
15 | .more { | 44 | color: var(--mainForegroundColor); |
16 | display: block; | ||
17 | } | ||
18 | } | 45 | } |
19 | 46 | ||
20 | .position { | 47 | .video-info-name { |
48 | font-size: 18px; | ||
21 | font-weight: $font-semibold; | 49 | font-weight: $font-semibold; |
22 | margin-right: 10px; | ||
23 | color: $grey-foreground-color; | ||
24 | } | 50 | } |
25 | 51 | ||
26 | my-video-thumbnail { | 52 | .video-info-account, .video-info-timestamp { |
27 | display: flex; // Avoids an issue with line-height that adds space below the element | 53 | color: $grey-foreground-color; |
28 | margin-right: 10px; | ||
29 | |||
30 | /deep/ .video-thumbnail { | ||
31 | @include miniature-thumbnail(130px, 72px); | ||
32 | } | ||
33 | } | 54 | } |
55 | } | ||
34 | 56 | ||
35 | .video-info { | 57 | .more { |
36 | display: flex; | 58 | justify-self: flex-end; |
37 | flex-direction: column; | 59 | margin-left: auto; |
60 | cursor: pointer; | ||
61 | display: none; | ||
38 | 62 | ||
39 | a { | 63 | &.show { |
40 | @include disable-default-a-behaviour; | 64 | display: block; |
65 | } | ||
41 | 66 | ||
42 | color: var(--mainForegroundColor); | 67 | .icon-more { |
43 | } | 68 | @include apply-svg-color($grey-foreground-color); |
44 | 69 | ||
45 | .video-info-name { | 70 | &::after { |
46 | font-size: 18px; | 71 | border: none; |
47 | font-weight: $font-semibold; | ||
48 | } | 72 | } |
73 | } | ||
49 | 74 | ||
50 | .video-info-account, .video-info-timestamp { | 75 | .dropdown-item { |
51 | color: $grey-foreground-color; | 76 | @include dropdown-with-icon-item; |
52 | } | ||
53 | } | 77 | } |
54 | 78 | ||
55 | .more { | 79 | .timestamp-options { |
56 | justify-self: flex-end; | 80 | padding-top: 0; |
57 | margin-left: auto; | 81 | padding-left: 35px; |
58 | cursor: pointer; | 82 | margin-bottom: 15px; |
59 | display: none; | ||
60 | 83 | ||
61 | &.show { | 84 | > div { |
62 | display: block; | 85 | display: flex; |
86 | align-items: center; | ||
63 | } | 87 | } |
64 | 88 | ||
65 | .icon-more { | 89 | input { |
66 | @include apply-svg-color($grey-foreground-color); | 90 | @include peertube-button; |
91 | @include orange-button; | ||
67 | 92 | ||
68 | &::after { | 93 | margin-top: 10px; |
69 | border: none; | ||
70 | } | ||
71 | } | 94 | } |
95 | } | ||
96 | } | ||
97 | } | ||
72 | 98 | ||
73 | .dropdown-item { | 99 | // Thanks Angular CDK <3 https://material.angular.io/cdk/drag-drop/examples |
74 | @include dropdown-with-icon-item; | 100 | .cdk-drag-preview { |
75 | } | 101 | box-sizing: border-box; |
102 | border-radius: 4px; | ||
103 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), | ||
104 | 0 8px 10px 1px rgba(0, 0, 0, 0.14), | ||
105 | 0 3px 14px 2px rgba(0, 0, 0, 0.12); | ||
106 | } | ||
76 | 107 | ||
77 | .timestamp-options { | 108 | .cdk-drag-placeholder { |
78 | padding-top: 0; | 109 | opacity: 0; |
79 | padding-left: 35px; | 110 | } |
80 | margin-bottom: 15px; | ||
81 | 111 | ||
82 | > div { | 112 | .cdk-drag-animating { |
83 | display: flex; | 113 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); |
84 | align-items: center; | 114 | } |
85 | } | ||
86 | 115 | ||
87 | input { | 116 | .video:last-child { |
88 | @include peertube-button; | 117 | border: none; |
89 | @include orange-button; | 118 | } |
90 | 119 | ||
91 | margin-top: 10px; | 120 | .videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) { |
92 | } | 121 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); |
93 | } | ||
94 | } | ||
95 | } | ||
96 | } | 122 | } |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts index 76aff3d4f..4076a3721 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts | |||
@@ -4,7 +4,7 @@ import { AuthService } from '../../core/auth' | |||
4 | import { ConfirmService } from '../../core/confirm' | 4 | import { ConfirmService } from '../../core/confirm' |
5 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 5 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
6 | import { Video } from '@app/shared/video/video.model' | 6 | import { Video } from '@app/shared/video/video.model' |
7 | import { Subscription } from 'rxjs' | 7 | import { Subject, Subscription } from 'rxjs' |
8 | import { ActivatedRoute } from '@angular/router' | 8 | import { ActivatedRoute } from '@angular/router' |
9 | import { VideoService } from '@app/shared/video/video.service' | 9 | import { VideoService } from '@app/shared/video/video.service' |
10 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 10 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
@@ -13,6 +13,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
13 | import { secondsToTime } from '../../../assets/player/utils' | 13 | import { secondsToTime } from '../../../assets/player/utils' |
14 | import { VideoPlaylistElementUpdate } from '@shared/models' | 14 | import { VideoPlaylistElementUpdate } from '@shared/models' |
15 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | 15 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' |
16 | import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop' | ||
17 | import { throttleTime } from 'rxjs/operators' | ||
16 | 18 | ||
17 | @Component({ | 19 | @Component({ |
18 | selector: 'my-account-video-playlist-elements', | 20 | selector: 'my-account-video-playlist-elements', |
@@ -42,6 +44,7 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
42 | 44 | ||
43 | private videoPlaylistId: string | number | 45 | private videoPlaylistId: string | number |
44 | private paramsSub: Subscription | 46 | private paramsSub: Subscription |
47 | private dragMoveSubject = new Subject<number>() | ||
45 | 48 | ||
46 | constructor ( | 49 | constructor ( |
47 | private authService: AuthService, | 50 | private authService: AuthService, |
@@ -61,12 +64,66 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
61 | 64 | ||
62 | this.loadPlaylistInfo() | 65 | this.loadPlaylistInfo() |
63 | }) | 66 | }) |
67 | |||
68 | this.dragMoveSubject.asObservable() | ||
69 | .pipe(throttleTime(200)) | ||
70 | .subscribe(y => this.checkScroll(y)) | ||
64 | } | 71 | } |
65 | 72 | ||
66 | ngOnDestroy () { | 73 | ngOnDestroy () { |
67 | if (this.paramsSub) this.paramsSub.unsubscribe() | 74 | if (this.paramsSub) this.paramsSub.unsubscribe() |
68 | } | 75 | } |
69 | 76 | ||
77 | drop (event: CdkDragDrop<any>) { | ||
78 | const previousIndex = event.previousIndex | ||
79 | const newIndex = event.currentIndex | ||
80 | |||
81 | if (previousIndex === newIndex) return | ||
82 | |||
83 | const oldPosition = this.videos[previousIndex].playlistElement.position | ||
84 | const insertAfter = newIndex === 0 ? 0 : this.videos[newIndex].playlistElement.position | ||
85 | |||
86 | this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter) | ||
87 | .subscribe( | ||
88 | () => { /* nothing to do */ }, | ||
89 | |||
90 | err => this.notifier.error(err.message) | ||
91 | ) | ||
92 | |||
93 | const video = this.videos[previousIndex] | ||
94 | |||
95 | this.videos.splice(previousIndex, 1) | ||
96 | this.videos.splice(newIndex, 0, video) | ||
97 | |||
98 | this.reorderClientPositions() | ||
99 | } | ||
100 | |||
101 | onDragMove (event: CdkDragMove<any>) { | ||
102 | this.dragMoveSubject.next(event.pointerPosition.y) | ||
103 | } | ||
104 | |||
105 | checkScroll (pointerY: number) { | ||
106 | // FIXME: Uncomment when https://github.com/angular/material2/issues/14098 is fixed | ||
107 | // FIXME: Remove when https://github.com/angular/material2/issues/13588 is implemented | ||
108 | // if (pointerY < 150) { | ||
109 | // window.scrollBy({ | ||
110 | // left: 0, | ||
111 | // top: -20, | ||
112 | // behavior: 'smooth' | ||
113 | // }) | ||
114 | // | ||
115 | // return | ||
116 | // } | ||
117 | // | ||
118 | // if (window.innerHeight - pointerY <= 50) { | ||
119 | // window.scrollBy({ | ||
120 | // left: 0, | ||
121 | // top: 20, | ||
122 | // behavior: 'smooth' | ||
123 | // }) | ||
124 | // } | ||
125 | } | ||
126 | |||
70 | isVideoBlur (video: Video) { | 127 | isVideoBlur (video: Video) { |
71 | return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig()) | 128 | return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig()) |
72 | } | 129 | } |
@@ -78,6 +135,7 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
78 | this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName })) | 135 | this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName })) |
79 | 136 | ||
80 | this.videos = this.videos.filter(v => v.id !== video.id) | 137 | this.videos = this.videos.filter(v => v.id !== video.id) |
138 | this.reorderClientPositions() | ||
81 | }, | 139 | }, |
82 | 140 | ||
83 | err => this.notifier.error(err.message) | 141 | err => this.notifier.error(err.message) |
@@ -173,4 +231,13 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
173 | this.playlist = playlist | 231 | this.playlist = playlist |
174 | }) | 232 | }) |
175 | } | 233 | } |
234 | |||
235 | private reorderClientPositions () { | ||
236 | let i = 1 | ||
237 | |||
238 | for (const video of this.videos) { | ||
239 | video.playlistElement.position = i | ||
240 | i++ | ||
241 | } | ||
242 | } | ||
176 | } | 243 | } |
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index ba8300111..4a18a9968 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -35,6 +35,7 @@ import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-vi | |||
35 | import { | 35 | import { |
36 | MyAccountVideoPlaylistElementsComponent | 36 | MyAccountVideoPlaylistElementsComponent |
37 | } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component' | 37 | } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component' |
38 | import { DragDropModule } from '@angular/cdk/drag-drop' | ||
38 | 39 | ||
39 | @NgModule({ | 40 | @NgModule({ |
40 | imports: [ | 41 | imports: [ |
@@ -43,7 +44,8 @@ import { | |||
43 | AutoCompleteModule, | 44 | AutoCompleteModule, |
44 | SharedModule, | 45 | SharedModule, |
45 | TableModule, | 46 | TableModule, |
46 | InputSwitchModule | 47 | InputSwitchModule, |
48 | DragDropModule | ||
47 | ], | 49 | ], |
48 | 50 | ||
49 | declarations: [ | 51 | declarations: [ |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 1f9eee0b7..05da0d829 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -9,7 +9,6 @@ import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.d | |||
9 | 9 | ||
10 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' | 10 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' |
11 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' | 11 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' |
12 | import { KeyFilterModule } from 'primeng/keyfilter' | ||
13 | 12 | ||
14 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | 13 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' |
15 | import { ButtonComponent } from './buttons/button.component' | 14 | import { ButtonComponent } from './buttons/button.component' |
@@ -95,7 +94,6 @@ import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.compo | |||
95 | 94 | ||
96 | PrimeSharedModule, | 95 | PrimeSharedModule, |
97 | InputMaskModule, | 96 | InputMaskModule, |
98 | KeyFilterModule, | ||
99 | NgPipesModule | 97 | NgPipesModule |
100 | ], | 98 | ], |
101 | 99 | ||
@@ -155,7 +153,6 @@ import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.compo | |||
155 | 153 | ||
156 | PrimeSharedModule, | 154 | PrimeSharedModule, |
157 | InputMaskModule, | 155 | InputMaskModule, |
158 | KeyFilterModule, | ||
159 | BytesPipe, | 156 | BytesPipe, |
160 | KeysPipe, | 157 | KeysPipe, |
161 | 158 | ||
diff --git a/client/src/app/shared/video-playlist/video-playlist.service.ts b/client/src/app/shared/video-playlist/video-playlist.service.ts index f7b37f83a..da7437507 100644 --- a/client/src/app/shared/video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/video-playlist/video-playlist.service.ts | |||
@@ -17,6 +17,7 @@ import { AccountService } from '@app/shared/account/account.service' | |||
17 | import { Account } from '@app/shared/account/account.model' | 17 | import { Account } from '@app/shared/account/account.model' |
18 | import { RestService } from '@app/shared/rest' | 18 | import { RestService } from '@app/shared/rest' |
19 | import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' | 19 | import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' |
20 | import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' | ||
20 | 21 | ||
21 | @Injectable() | 22 | @Injectable() |
22 | export class VideoPlaylistService { | 23 | export class VideoPlaylistService { |
@@ -125,6 +126,19 @@ export class VideoPlaylistService { | |||
125 | ) | 126 | ) |
126 | } | 127 | } |
127 | 128 | ||
129 | reorderPlaylist (playlistId: number, oldPosition: number, newPosition: number) { | ||
130 | const body: VideoPlaylistReorder = { | ||
131 | startPosition: oldPosition, | ||
132 | insertAfterPosition: newPosition | ||
133 | } | ||
134 | |||
135 | return this.authHttp.post(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/reorder', body) | ||
136 | .pipe( | ||
137 | map(this.restExtractor.extractDataBool), | ||
138 | catchError(err => this.restExtractor.handleError(err)) | ||
139 | ) | ||
140 | } | ||
141 | |||
128 | doesVideoExistInPlaylist (videoId: number) { | 142 | doesVideoExistInPlaylist (videoId: number) { |
129 | this.videoExistsInPlaylistSubject.next(videoId) | 143 | this.videoExistsInPlaylistSubject.next(videoId) |
130 | 144 | ||