aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+my-library/my-video-playlists
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-11-12 15:28:54 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-11-13 12:02:21 +0100
commit17119e4a546522468878cf115558b17949ab50d0 (patch)
tree3f130cfd7fdccf5aeeac9beee941750590239047 /client/src/app/+my-library/my-video-playlists
parentb4bc269e5517849b5b89052f0c1a2c01b6f65089 (diff)
downloadPeerTube-17119e4a546522468878cf115558b17949ab50d0.tar.gz
PeerTube-17119e4a546522468878cf115558b17949ab50d0.tar.zst
PeerTube-17119e4a546522468878cf115558b17949ab50d0.zip
Reorganize left menu and account menu
Add my-settings and my-library in left menu Move administration below my-library Split account menu: my-setting and my library
Diffstat (limited to 'client/src/app/+my-library/my-video-playlists')
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts91
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html100
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss36
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.ts13
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html51
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss83
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts197
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts129
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html35
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss78
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts101
11 files changed, 914 insertions, 0 deletions
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts
new file mode 100644
index 000000000..5abea54b0
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts
@@ -0,0 +1,91 @@
1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router'
3import { AuthService, Notifier, ServerService } from '@app/core'
4import { populateAsyncUserVideoChannels } from '@app/helpers'
5import {
6 setPlaylistChannelValidator,
7 VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
8 VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR,
9 VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
10 VIDEO_PLAYLIST_PRIVACY_VALIDATOR
11} from '@app/shared/form-validators/video-playlist-validators'
12import { FormValidatorService } from '@app/shared/shared-forms'
13import { VideoPlaylistService } from '@app/shared/shared-video-playlist'
14import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
15import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
16import { MyVideoPlaylistEdit } from './my-video-playlist-edit'
17
18@Component({
19 templateUrl: './my-video-playlist-edit.component.html',
20 styleUrls: [ './my-video-playlist-edit.component.scss' ]
21})
22export class MyVideoPlaylistCreateComponent extends MyVideoPlaylistEdit implements OnInit {
23 error: string
24
25 constructor (
26 protected formValidatorService: FormValidatorService,
27 private authService: AuthService,
28 private notifier: Notifier,
29 private router: Router,
30 private videoPlaylistService: VideoPlaylistService,
31 private serverService: ServerService
32 ) {
33 super()
34 }
35
36 ngOnInit () {
37 this.buildForm({
38 displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
39 privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR,
40 description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR,
41 videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
42 thumbnailfile: null
43 })
44
45 this.form.get('privacy').valueChanges.subscribe(privacy => {
46 setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy)
47 })
48
49 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
50 .catch(err => console.error('Cannot populate user video channels.', err))
51
52 this.serverService.getVideoPlaylistPrivacies()
53 .subscribe(videoPlaylistPrivacies => {
54 this.videoPlaylistPrivacies = videoPlaylistPrivacies
55
56 this.form.patchValue({
57 privacy: VideoPlaylistPrivacy.PRIVATE
58 })
59 })
60 }
61
62 formValidated () {
63 this.error = undefined
64
65 const body = this.form.value
66 const videoPlaylistCreate: VideoPlaylistCreate = {
67 displayName: body.displayName,
68 privacy: body.privacy,
69 description: body.description || null,
70 videoChannelId: body.videoChannelId || null,
71 thumbnailfile: body.thumbnailfile || null
72 }
73
74 this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate).subscribe(
75 () => {
76 this.notifier.success($localize`Playlist ${videoPlaylistCreate.displayName} created.`)
77 this.router.navigate([ '/my-library', 'video-playlists' ])
78 },
79
80 err => this.error = err.message
81 )
82 }
83
84 isCreation () {
85 return true
86 }
87
88 getFormButtonTitle () {
89 return $localize`Create`
90 }
91}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html
new file mode 100644
index 000000000..0d8d2a447
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html
@@ -0,0 +1,100 @@
1<nav aria-label="breadcrumb">
2 <ol class="breadcrumb">
3 <li class="breadcrumb-item">
4 <a routerLink="/my-library/video-playlists" i18n>My Playlists</a>
5 </li>
6
7 <ng-container *ngIf="isCreation()">
8 <li class="breadcrumb-item active" i18n>Create</li>
9 </ng-container>
10 <ng-container *ngIf="!isCreation()">
11 <li class="breadcrumb-item active" i18n>Edit</li>
12 <li class="breadcrumb-item active" aria-current="page">
13 <a *ngIf="videoPlaylistToUpdate" [routerLink]="[ '/my-library/video-playlists/update', videoPlaylistToUpdate?.uuid ]">{{ videoPlaylistToUpdate?.displayName }}</a>
14 </li>
15 </ng-container>
16 </ol>
17</nav>
18
19<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
20
21<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
22
23 <div class="form-row"> <!-- playlist grid -->
24 <div class="form-group col-12 col-lg-4 col-xl-3">
25 <div *ngIf="isCreation()" class="video-playlist-title" i18n>NEW PLAYLIST</div>
26 <div *ngIf="!isCreation() && videoPlaylistToUpdate" class="video-playlist-title" i18n>PLAYLIST</div>
27 </div>
28
29 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
30
31 <div class="col-md-12 col-xl-6">
32 <div class="form-group">
33 <label i18n for="displayName">Display name</label>
34 <input
35 type="text" id="displayName" class="form-control"
36 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
37 >
38 <div *ngIf="formErrors['displayName']" class="form-error">
39 {{ formErrors['displayName'] }}
40 </div>
41 </div>
42
43 <div class="form-group">
44 <label i18n for="description">Description</label>
45 <textarea
46 id="description" formControlName="description"
47 class="form-control" [ngClass]="{ 'input-error': formErrors['description'] }"
48 ></textarea>
49 <div *ngIf="formErrors.description" class="form-error">
50 {{ formErrors.description }}
51 </div>
52 </div>
53 </div>
54
55 <div class="col-md-12 col-xl-6">
56 <div class="form-group">
57 <label i18n for="privacy">Privacy</label>
58 <div class="peertube-select-container">
59 <select id="privacy" formControlName="privacy" class="form-control">
60 <option *ngFor="let privacy of videoPlaylistPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
61 </select>
62 </div>
63
64 <div *ngIf="formErrors.privacy" class="form-error">
65 {{ formErrors.privacy }}
66 </div>
67 </div>
68
69 <div class="form-group">
70 <label i18n>Channel</label>
71
72 <my-select-channel
73 labelForId="videoChannelIdl" [items]="userVideoChannels" formControlName="videoChannelId"
74 ></my-select-channel>
75
76 <div *ngIf="formErrors['videoChannelId']" class="form-error">
77 {{ formErrors['videoChannelId'] }}
78 </div>
79 </div>
80
81 <div class="form-group">
82 <label i18n>Playlist thumbnail</label>
83
84 <my-preview-upload
85 i18n-inputLabel inputLabel="Edit" inputName="thumbnailfile" formControlName="thumbnailfile"
86 previewWidth="223px" previewHeight="122px"
87 ></my-preview-upload>
88 </div>
89 </div>
90
91 <div class="form-row"> <!-- submit placement block -->
92 <div class="col-md-7 col-xl-5"></div>
93 <div class="col-md-5 col-xl-5 d-inline-flex">
94 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
95 </div>
96 </div>
97 </div>
98 </div>
99
100</form>
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss
new file mode 100644
index 000000000..08fab1101
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss
@@ -0,0 +1,36 @@
1@import '_variables';
2@import '_mixins';
3
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9.video-playlist-title {
10 @include settings-big-title;
11}
12
13input[type=text] {
14 @include peertube-input-text(340px);
15
16 display: block;
17}
18
19textarea {
20 @include peertube-textarea(500px, 150px);
21
22 display: block;
23}
24
25.peertube-select-container {
26 @include peertube-select-container(340px);
27}
28
29input[type=submit] {
30 @include peertube-button;
31 @include orange-button;
32}
33
34.breadcrumb {
35 @include breadcrumb;
36}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.ts
new file mode 100644
index 000000000..40ba23e75
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.ts
@@ -0,0 +1,13 @@
1import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms'
2import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models'
3import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
4
5export abstract class MyVideoPlaylistEdit extends FormReactive {
6 // Declare it here to avoid errors in create template
7 videoPlaylistToUpdate: VideoPlaylist
8 userVideoChannels: SelectChannelItem[] = []
9 videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
10
11 abstract isCreation (): boolean
12 abstract getFormButtonTitle (): string
13}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html
new file mode 100644
index 000000000..09b4c8a1b
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html
@@ -0,0 +1,51 @@
1<div class="row">
2
3 <div class="playlist-info col-xs-12 col-md-5 col-xl-3">
4 <my-video-playlist-miniature
5 *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true"
6 [displayDescription]="true" [displayPrivacy]="true"
7 ></my-video-playlist-miniature>
8
9 <div class="playlist-buttons">
10 <button (click)="showShareModal()" class="action-button share-button">
11 <my-global-icon iconName="share" aria-hidden="true"></my-global-icon>
12 <span class="icon-text" i18n>Share</span>
13 </button>
14
15 <my-action-dropdown
16 *ngIf="isRegularPlaylist(playlist)"
17 [entry]="playlist" [actions]="playlistActions" label="More"
18 ></my-action-dropdown>
19 </div>
20
21 </div>
22
23 <div class="playlist-elements col-xs-12 col-md-7 col-xl-9">
24 <div class="no-results" *ngIf="pagination.totalItems === 0">
25 <div i18n>No videos in this playlist.</div>
26
27 <div i18n>
28 Browse videos on PeerTube to add them in your playlist.
29 </div>
30
31 <div i18n>
32 See the <a target="_blank" href="https://docs.joinpeertube.org/#/use-library?id=playlist">documentation</a> for more information.
33 </div>
34 </div>
35
36 <div
37 class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"
38 cdkDropList (cdkDropListDropped)="drop($event)" [dataObservable]="onDataSubject.asObservable()"
39 >
40 <div class="video" *ngFor="let playlistElement of playlistElements; trackBy: trackByFn" cdkDrag [cdkDragStartDelay]="getDragStartDelay()">
41 <my-video-playlist-element-miniature
42 [playlistElement]="playlistElement" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)"
43 [position]="playlistElement.position"
44 >
45 </my-video-playlist-element-miniature>
46 </div>
47 </div>
48 </div>
49</div>
50
51<my-video-share #videoShareModal [playlist]="playlist"></my-video-share>
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss
new file mode 100644
index 000000000..de7e1993f
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss
@@ -0,0 +1,83 @@
1@import '_variables';
2@import '_mixins';
3@import '_miniature';
4
5.playlist-info {
6 background-color: pvar(--submenuColor);
7 margin-left: -$not-expanded-horizontal-margins;
8 margin-top: -$sub-menu-margin-bottom;
9
10 padding: 10px;
11
12 display: flex;
13 flex-direction: column;
14 justify-content: flex-start;
15 align-items: center;
16
17 /* fix ellipsis dots background color */
18 ::ng-deep .miniature-name::after {
19 background-color: pvar(--submenuColor) !important;
20 }
21}
22
23.playlist-buttons {
24 display:flex;
25 margin: 30px 0 10px 0;
26
27 .share-button {
28 @include peertube-button;
29 @include button-with-icon(17px, 3px, -1px);
30 @include grey-button;
31 @include apply-svg-color(pvar(--actionButtonColor));
32
33 margin-right: 10px;
34 }
35}
36
37// Thanks Angular CDK <3 https://material.angular.io/cdk/drag-drop/examples
38.cdk-drag-preview {
39 box-sizing: border-box;
40 border-radius: 4px;
41 box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
42 0 8px 10px 1px rgba(0, 0, 0, 0.14),
43 0 3px 14px 2px rgba(0, 0, 0, 0.12);
44}
45
46.cdk-drag-placeholder {
47 opacity: 0;
48}
49
50.cdk-drag-animating {
51 transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
52}
53
54.video:last-child {
55 border: none;
56}
57
58.videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) {
59 transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
60}
61
62@media screen and (max-width: $small-view) {
63 .playlist-info {
64 width: 100vw;
65 padding-top: 20px;
66 margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1);
67 }
68
69 .playlist-elements {
70 padding: 0 !important;
71 }
72
73 ::ng-deep my-video-playlist-element-miniature {
74
75 .video {
76 padding: 5px !important;
77 }
78
79 .position {
80 margin-right: 5px !important;
81 }
82 }
83}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts
new file mode 100644
index 000000000..a8fdf6e29
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts
@@ -0,0 +1,197 @@
1import { Subject, Subscription } from 'rxjs'
2import { CdkDragDrop } from '@angular/cdk/drag-drop'
3import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5import { ComponentPagination, ConfirmService, Notifier, ScreenService } from '@app/core'
6import { DropdownAction } from '@app/shared/shared-main'
7import { VideoShareComponent } from '@app/shared/shared-share-modal'
8import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist'
9import { VideoPlaylistType } from '@shared/models'
10
11@Component({
12 templateUrl: './my-video-playlist-elements.component.html',
13 styleUrls: [ './my-video-playlist-elements.component.scss' ]
14})
15export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy {
16 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
17
18 playlistElements: VideoPlaylistElement[] = []
19 playlist: VideoPlaylist
20
21 playlistActions: DropdownAction<VideoPlaylist>[][] = []
22
23 pagination: ComponentPagination = {
24 currentPage: 1,
25 itemsPerPage: 10,
26 totalItems: null
27 }
28
29 onDataSubject = new Subject<any[]>()
30
31 private videoPlaylistId: string | number
32 private paramsSub: Subscription
33
34 constructor (
35 private notifier: Notifier,
36 private router: Router,
37 private confirmService: ConfirmService,
38 private route: ActivatedRoute,
39 private screenService: ScreenService,
40 private videoPlaylistService: VideoPlaylistService
41 ) {}
42
43 ngOnInit () {
44 this.playlistActions = [
45 [
46 {
47 label: $localize`Update playlist`,
48 iconName: 'edit',
49 linkBuilder: playlist => [ '/my-library', 'video-playlists', 'update', playlist.uuid ]
50 },
51 {
52 label: $localize`Delete playlist`,
53 iconName: 'delete',
54 handler: playlist => this.deleteVideoPlaylist(playlist)
55 }
56 ]
57 ]
58
59 this.paramsSub = this.route.params.subscribe(routeParams => {
60 this.videoPlaylistId = routeParams[ 'videoPlaylistId' ]
61 this.loadElements()
62
63 this.loadPlaylistInfo()
64 })
65 }
66
67 ngOnDestroy () {
68 if (this.paramsSub) this.paramsSub.unsubscribe()
69 }
70
71 drop (event: CdkDragDrop<any>) {
72 const previousIndex = event.previousIndex
73 const newIndex = event.currentIndex
74
75 if (previousIndex === newIndex) return
76
77 const oldPosition = this.playlistElements[previousIndex].position
78 let insertAfter = this.playlistElements[newIndex].position
79
80 if (oldPosition > insertAfter) insertAfter--
81
82 const element = this.playlistElements[previousIndex]
83
84 this.playlistElements.splice(previousIndex, 1)
85 this.playlistElements.splice(newIndex, 0, element)
86
87 this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter)
88 .subscribe(
89 () => {
90 this.reorderClientPositions()
91 },
92
93 err => this.notifier.error(err.message)
94 )
95 }
96
97 onElementRemoved (element: VideoPlaylistElement) {
98 const oldFirst = this.findFirst()
99
100 this.playlistElements = this.playlistElements.filter(v => v.id !== element.id)
101 this.reorderClientPositions(oldFirst)
102 }
103
104 onNearOfBottom () {
105 // Last page
106 if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
107
108 this.pagination.currentPage += 1
109 this.loadElements()
110 }
111
112 trackByFn (index: number, elem: VideoPlaylistElement) {
113 return elem.id
114 }
115
116 isRegularPlaylist (playlist: VideoPlaylist) {
117 return playlist?.type.id === VideoPlaylistType.REGULAR
118 }
119
120 showShareModal () {
121 this.videoShareModal.show()
122 }
123
124 async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) {
125 const res = await this.confirmService.confirm(
126 $localize`Do you really want to delete ${videoPlaylist.displayName}?`,
127 $localize`Delete`
128 )
129 if (res === false) return
130
131 this.videoPlaylistService.removeVideoPlaylist(videoPlaylist)
132 .subscribe(
133 () => {
134 this.router.navigate([ '/my-library', 'video-playlists' ])
135 this.notifier.success($localize`Playlist ${videoPlaylist.displayName} deleted.`)
136 },
137
138 error => this.notifier.error(error.message)
139 )
140 }
141
142 /**
143 * Returns null to not have drag and drop delay.
144 * In small views, where elements are about 100% wide,
145 * we add a delay to prevent unwanted drag&drop.
146 *
147 * @see {@link https://github.com/Chocobozzz/PeerTube/issues/2078}
148 *
149 * @returns {null|number} Null for no delay, or a number in milliseconds.
150 */
151 getDragStartDelay (): null | number {
152 if (this.screenService.isInTouchScreen()) {
153 return 500
154 }
155
156 return null
157 }
158
159 private loadElements () {
160 this.videoPlaylistService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
161 .subscribe(({ total, data }) => {
162 this.playlistElements = this.playlistElements.concat(data)
163 this.pagination.totalItems = total
164
165 this.onDataSubject.next(data)
166 })
167 }
168
169 private loadPlaylistInfo () {
170 this.videoPlaylistService.getVideoPlaylist(this.videoPlaylistId)
171 .subscribe(playlist => {
172 this.playlist = playlist
173 })
174 }
175
176 private reorderClientPositions (first?: VideoPlaylistElement) {
177 if (this.playlistElements.length === 0) return
178
179 const oldFirst = first || this.findFirst()
180 let i = 1
181
182 for (const element of this.playlistElements) {
183 element.position = i
184 i++
185 }
186
187 // Reload playlist thumbnail if the first element changed
188 const newFirst = this.findFirst()
189 if (oldFirst && newFirst && oldFirst.id !== newFirst.id) {
190 this.playlist.refreshThumbnail()
191 }
192 }
193
194 private findFirst () {
195 return this.playlistElements.find(e => e.position === 1)
196 }
197}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts
new file mode 100644
index 000000000..532423ba2
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts
@@ -0,0 +1,129 @@
1import { forkJoin, Subscription } from 'rxjs'
2import { map, switchMap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, Notifier, ServerService } from '@app/core'
6import { populateAsyncUserVideoChannels } from '@app/helpers'
7import {
8 setPlaylistChannelValidator,
9 VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
10 VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR,
11 VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
12 VIDEO_PLAYLIST_PRIVACY_VALIDATOR
13} from '@app/shared/form-validators/video-playlist-validators'
14import { FormValidatorService } from '@app/shared/shared-forms'
15import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
16import { VideoPlaylistUpdate } from '@shared/models'
17import { MyVideoPlaylistEdit } from './my-video-playlist-edit'
18
19@Component({
20 templateUrl: './my-video-playlist-edit.component.html',
21 styleUrls: [ './my-video-playlist-edit.component.scss' ]
22})
23export class MyVideoPlaylistUpdateComponent extends MyVideoPlaylistEdit implements OnInit, OnDestroy {
24 error: string
25 videoPlaylistToUpdate: VideoPlaylist
26
27 private paramsSub: Subscription
28
29 constructor (
30 protected formValidatorService: FormValidatorService,
31 private authService: AuthService,
32 private notifier: Notifier,
33 private router: Router,
34 private route: ActivatedRoute,
35 private videoPlaylistService: VideoPlaylistService,
36 private serverService: ServerService
37 ) {
38 super()
39 }
40
41 ngOnInit () {
42 this.buildForm({
43 displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
44 privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR,
45 description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR,
46 videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
47 thumbnailfile: null
48 })
49
50 this.form.get('privacy').valueChanges.subscribe(privacy => {
51 setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy)
52 })
53
54 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
55 .catch(err => console.error('Cannot populate user video channels.', err))
56
57 this.paramsSub = this.route.params
58 .pipe(
59 map(routeParams => routeParams['videoPlaylistId']),
60 switchMap(videoPlaylistId => {
61 return forkJoin([
62 this.videoPlaylistService.getVideoPlaylist(videoPlaylistId),
63 this.serverService.getVideoPlaylistPrivacies()
64 ])
65 })
66 )
67 .subscribe(
68 ([ videoPlaylistToUpdate, videoPlaylistPrivacies]) => {
69 this.videoPlaylistToUpdate = videoPlaylistToUpdate
70 this.videoPlaylistPrivacies = videoPlaylistPrivacies
71
72 this.hydrateFormFromPlaylist()
73 },
74
75 err => this.error = err.message
76 )
77 }
78
79 ngOnDestroy () {
80 if (this.paramsSub) this.paramsSub.unsubscribe()
81 }
82
83 formValidated () {
84 this.error = undefined
85
86 const body = this.form.value
87 const videoPlaylistUpdate: VideoPlaylistUpdate = {
88 displayName: body.displayName,
89 privacy: body.privacy,
90 description: body.description || null,
91 videoChannelId: body.videoChannelId || null,
92 thumbnailfile: body.thumbnailfile || undefined
93 }
94
95 this.videoPlaylistService.updateVideoPlaylist(this.videoPlaylistToUpdate, videoPlaylistUpdate).subscribe(
96 () => {
97 this.notifier.success($localize`Playlist ${videoPlaylistUpdate.displayName} updated.`)
98 this.router.navigate([ '/my-library', 'video-playlists' ])
99 },
100
101 err => this.error = err.message
102 )
103 }
104
105 isCreation () {
106 return false
107 }
108
109 getFormButtonTitle () {
110 return $localize`Update`
111 }
112
113 private hydrateFormFromPlaylist () {
114 this.form.patchValue({
115 displayName: this.videoPlaylistToUpdate.displayName,
116 privacy: this.videoPlaylistToUpdate.privacy.id,
117 description: this.videoPlaylistToUpdate.description,
118 videoChannelId: this.videoPlaylistToUpdate.videoChannel ? this.videoPlaylistToUpdate.videoChannel.id : null
119 })
120
121 fetch(this.videoPlaylistToUpdate.thumbnailUrl)
122 .then(response => response.blob())
123 .then(data => {
124 this.form.patchValue({
125 thumbnailfile: data
126 })
127 })
128 }
129}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
new file mode 100644
index 000000000..afcf6a084
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
@@ -0,0 +1,35 @@
1<h1>
2 <span>
3 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span>
5 </span>
6</h1>
7
8<div class="video-playlists-header d-flex justify-content-between">
9 <div class="has-feedback has-clear">
10 <input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch"
11 (ngModelChange)="onVideoPlaylistSearchChanged()" />
12 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
13 <span class="sr-only" i18n>Clear filters</span>
14 </div>
15
16 <a class="create-button" routerLink="create">
17 <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
18 <ng-container i18n>Create playlist</ng-container>
19 </a>
20</div>
21
22<div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
23 <div *ngFor="let playlist of videoPlaylists" class="video-playlist">
24 <div class="miniature-wrapper">
25 <my-video-playlist-miniature [playlist]="playlist" [toManage]="true" [displayChannel]="true" [displayDescription]="true" [displayPrivacy]="true"
26 ></my-video-playlist-miniature>
27 </div>
28
29 <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons">
30 <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button>
31
32 <my-edit-button label [routerLink]="[ 'update', playlist.uuid ]"></my-edit-button>
33 </div>
34 </div>
35</div>
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss
new file mode 100644
index 000000000..2b7c88246
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss
@@ -0,0 +1,78 @@
1@import '_variables';
2@import '_mixins';
3
4.create-button {
5 @include create-button;
6}
7
8input[type=text] {
9 @include peertube-input-text(300px);
10}
11
12::ng-deep .action-button {
13 &.action-button-delete {
14 margin-right: 10px;
15 }
16}
17
18.video-playlist {
19 @include row-blocks;
20
21 .miniature-wrapper {
22 flex-grow: 1;
23
24 ::ng-deep .miniature {
25 display: flex;
26
27 .miniature-info {
28 margin-left: 10px;
29 width: auto;
30 }
31 }
32 }
33
34 .video-playlist-buttons {
35 min-width: 190px;
36 height: max-content;
37 }
38}
39
40.video-playlists-header {
41 margin-bottom: 30px;
42}
43
44@media screen and (max-width: $small-view) {
45 .video-playlists-header {
46 text-align: center;
47 }
48
49 .video-playlist {
50
51 .video-playlist-buttons {
52 margin-top: 10px;
53 }
54 }
55
56 my-video-playlist-miniature ::ng-deep .miniature {
57 flex-direction: column;
58
59 .miniature-info {
60 margin-left: 0 !important;
61 }
62
63 .miniature-name {
64 max-width: $video-thumbnail-width;
65 }
66 }
67}
68
69@media screen and (max-width: $mobile-view) {
70 .video-playlists-header {
71 flex-direction: column;
72
73 input[type=text] {
74 width: 100% !important;
75 margin-bottom: 12px;
76 }
77 }
78}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts
new file mode 100644
index 000000000..f6d394923
--- /dev/null
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts
@@ -0,0 +1,101 @@
1import { Subject } from 'rxjs'
2import { debounceTime, mergeMap } from 'rxjs/operators'
3import { Component, OnInit } from '@angular/core'
4import { AuthService, ComponentPagination, ConfirmService, Notifier, User } from '@app/core'
5import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
6import { VideoPlaylistType } from '@shared/models'
7
8@Component({
9 templateUrl: './my-video-playlists.component.html',
10 styleUrls: [ './my-video-playlists.component.scss' ]
11})
12export class MyVideoPlaylistsComponent implements OnInit {
13 videoPlaylistsSearch: string
14 videoPlaylists: VideoPlaylist[] = []
15 videoPlaylistSearchChanged = new Subject<string>()
16
17 pagination: ComponentPagination = {
18 currentPage: 1,
19 itemsPerPage: 5,
20 totalItems: null
21 }
22
23 onDataSubject = new Subject<any[]>()
24
25 private user: User
26
27 constructor (
28 private authService: AuthService,
29 private notifier: Notifier,
30 private confirmService: ConfirmService,
31 private videoPlaylistService: VideoPlaylistService
32 ) {}
33
34 ngOnInit () {
35 this.user = this.authService.getUser()
36
37 this.loadVideoPlaylists()
38
39 this.videoPlaylistSearchChanged
40 .pipe(
41 debounceTime(500))
42 .subscribe(() => {
43 this.loadVideoPlaylists(true)
44 })
45 }
46
47 async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) {
48 const res = await this.confirmService.confirm(
49 $localize`Do you really want to delete ${videoPlaylist.displayName}?`,
50 $localize`Delete`
51 )
52 if (res === false) return
53
54 this.videoPlaylistService.removeVideoPlaylist(videoPlaylist)
55 .subscribe(
56 () => {
57 this.videoPlaylists = this.videoPlaylists
58 .filter(p => p.id !== videoPlaylist.id)
59
60 this.notifier.success($localize`Playlist ${videoPlaylist.displayName}} deleted.`)
61 },
62
63 error => this.notifier.error(error.message)
64 )
65 }
66
67 isRegularPlaylist (playlist: VideoPlaylist) {
68 return playlist.type.id === VideoPlaylistType.REGULAR
69 }
70
71 onNearOfBottom () {
72 // Last page
73 if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
74
75 this.pagination.currentPage += 1
76 this.loadVideoPlaylists()
77 }
78
79 resetSearch () {
80 this.videoPlaylistsSearch = ''
81 this.onVideoPlaylistSearchChanged()
82 }
83
84 onVideoPlaylistSearchChanged () {
85 this.videoPlaylistSearchChanged.next()
86 }
87
88 private loadVideoPlaylists (reset = false) {
89 this.authService.userInformationLoaded
90 .pipe(mergeMap(() => {
91 return this.videoPlaylistService.listAccountPlaylists(this.user.account, this.pagination, '-updatedAt', this.videoPlaylistsSearch)
92 }))
93 .subscribe(res => {
94 if (reset) this.videoPlaylists = []
95 this.videoPlaylists = this.videoPlaylists.concat(res.data)
96 this.pagination.totalItems = res.total
97
98 this.onDataSubject.next(res.data)
99 })
100 }
101}