mirror of https://github.com/Chocobozzz/PeerTube
Add/update/delete/list my playlists
parent
d4c9f45b31
commit
830b4faff1
|
@ -15,6 +15,13 @@ import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blockli
|
|||
import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
|
||||
import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
|
||||
import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
|
||||
import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component'
|
||||
import {
|
||||
MyAccountVideoPlaylistCreateComponent
|
||||
} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component'
|
||||
import {
|
||||
MyAccountVideoPlaylistUpdateComponent
|
||||
} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component'
|
||||
|
||||
const myAccountRoutes: Routes = [
|
||||
{
|
||||
|
@ -36,6 +43,7 @@ const myAccountRoutes: Routes = [
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-channels',
|
||||
component: MyAccountVideoChannelsComponent,
|
||||
|
@ -63,6 +71,35 @@ const myAccountRoutes: Routes = [
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-playlists',
|
||||
component: MyAccountVideoPlaylistsComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: 'Account playlists'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'video-playlists/create',
|
||||
component: MyAccountVideoPlaylistCreateComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: 'Create new playlist'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'video-playlists/update/:videoPlaylistId',
|
||||
component: MyAccountVideoPlaylistUpdateComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: 'Update playlist'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'videos',
|
||||
component: MyAccountVideosComponent,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { UserSubscriptionService } from '@app/shared/user-subscription'
|
||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||
|
||||
|
@ -21,8 +20,7 @@ export class MyAccountSubscriptionsComponent implements OnInit {
|
|||
|
||||
constructor (
|
||||
private userSubscriptionService: UserSubscriptionService,
|
||||
private notifier: Notifier,
|
||||
private i18n: I18n
|
||||
private notifier: Notifier
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="video-channels-header">
|
||||
<a class="create-button" routerLink="create">
|
||||
<my-global-icon iconName="add"></my-global-icon>
|
||||
<ng-container i18n>Create another video channel</ng-container>
|
||||
<ng-container i18n>Create a new video channel</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { VideoPlaylistValidatorsService } from '@app/shared'
|
||||
import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
|
||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||
import { VideoConstant } from '@shared/models'
|
||||
import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
|
||||
import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-video-playlist-create',
|
||||
templateUrl: './my-account-video-playlist-edit.component.html',
|
||||
styleUrls: [ './my-account-video-playlist-edit.component.scss' ]
|
||||
})
|
||||
export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit {
|
||||
error: string
|
||||
videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private authService: AuthService,
|
||||
private videoPlaylistValidatorsService: VideoPlaylistValidatorsService,
|
||||
private notifier: Notifier,
|
||||
private router: Router,
|
||||
private videoPlaylistService: VideoPlaylistService,
|
||||
private serverService: ServerService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
'display-name': this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME,
|
||||
privacy: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_PRIVACY,
|
||||
description: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DESCRIPTION,
|
||||
videoChannelId: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_CHANNEL_ID,
|
||||
thumbnailfile: null
|
||||
})
|
||||
|
||||
populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
|
||||
|
||||
this.serverService.videoPlaylistPrivaciesLoaded.subscribe(
|
||||
() => {
|
||||
this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies()
|
||||
|
||||
this.form.patchValue({
|
||||
privacy: VideoPlaylistPrivacy.PRIVATE
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
this.error = undefined
|
||||
|
||||
const body = this.form.value
|
||||
const videoPlaylistCreate: VideoPlaylistCreate = {
|
||||
displayName: body['display-name'],
|
||||
privacy: body.privacy,
|
||||
description: body.description || null,
|
||||
videoChannelId: body.videoChannelId || null,
|
||||
thumbnailfile: body.thumbnailfile || null
|
||||
}
|
||||
|
||||
this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate).subscribe(
|
||||
() => {
|
||||
this.notifier.success(
|
||||
this.i18n('Playlist {{playlistName}} created.', { playlistName: videoPlaylistCreate.displayName })
|
||||
)
|
||||
this.router.navigate([ '/my-account', 'video-playlists' ])
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
)
|
||||
}
|
||||
|
||||
isCreation () {
|
||||
return true
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
return this.i18n('Create')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<div i18n class="form-sub-title" *ngIf="isCreation() === true">Create a new playlist</div>
|
||||
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-xl-6">
|
||||
<div class="form-group">
|
||||
<label i18n for="display-name">Display name</label>
|
||||
<input
|
||||
type="text" id="display-name"
|
||||
formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
|
||||
>
|
||||
<div *ngIf="formErrors['display-name']" class="form-error">
|
||||
{{ formErrors['display-name'] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="description">Description</label>
|
||||
<textarea
|
||||
id="description" formControlName="description"
|
||||
[ngClass]="{ 'input-error': formErrors['description'] }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors.description" class="form-error">
|
||||
{{ formErrors.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 col-xl-6">
|
||||
<div class="form-group">
|
||||
<label i18n for="privacy">Privacy</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="privacy" formControlName="privacy">
|
||||
<option *ngFor="let privacy of videoPlaylistPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.privacy" class="form-error">
|
||||
{{ formErrors.privacy }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n>Channel</label>
|
||||
<div class="peertube-select-container">
|
||||
<select formControlName="videoChannelId">
|
||||
<option></option>
|
||||
<option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-image-upload
|
||||
i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
|
||||
previewWidth="200px" previewHeight="110px"
|
||||
></my-image-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||
</form>
|
|
@ -0,0 +1,27 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.form-sub-title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(340px);
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@include peertube-textarea(500px, 150px);
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
.peertube-select-container {
|
||||
@include peertube-select-container(340px);
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { FormReactive } from '@app/shared'
|
||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
import { ServerService } from '@app/core'
|
||||
import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
|
||||
|
||||
export abstract class MyAccountVideoPlaylistEdit extends FormReactive {
|
||||
// Declare it here to avoid errors in create template
|
||||
videoPlaylistToUpdate: VideoPlaylist
|
||||
userVideoChannels: { id: number, label: string }[] = []
|
||||
|
||||
abstract isCreation (): boolean
|
||||
abstract getFormButtonTitle (): string
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { MyAccountVideoPlaylistEdit } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-edit'
|
||||
import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
|
||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||
import { VideoPlaylistValidatorsService } from '@app/shared'
|
||||
import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model'
|
||||
import { VideoConstant } from '@shared/models'
|
||||
import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
|
||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-video-playlist-update',
|
||||
templateUrl: './my-account-video-playlist-edit.component.html',
|
||||
styleUrls: [ './my-account-video-playlist-edit.component.scss' ]
|
||||
})
|
||||
export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy {
|
||||
error: string
|
||||
videoPlaylistToUpdate: VideoPlaylist
|
||||
videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
|
||||
|
||||
private paramsSub: Subscription
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private authService: AuthService,
|
||||
private videoPlaylistValidatorsService: VideoPlaylistValidatorsService,
|
||||
private notifier: Notifier,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private videoPlaylistService: VideoPlaylistService,
|
||||
private i18n: I18n,
|
||||
private serverService: ServerService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
'display-name': this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME,
|
||||
privacy: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_PRIVACY,
|
||||
description: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DESCRIPTION,
|
||||
videoChannelId: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_CHANNEL_ID,
|
||||
thumbnailfile: null
|
||||
})
|
||||
|
||||
populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
|
||||
|
||||
this.paramsSub = this.route.params.subscribe(routeParams => {
|
||||
const videoPlaylistId = routeParams['videoPlaylistId']
|
||||
|
||||
this.videoPlaylistService.getVideoPlaylist(videoPlaylistId).subscribe(
|
||||
videoPlaylistToUpdate => {
|
||||
this.videoPlaylistToUpdate = videoPlaylistToUpdate
|
||||
|
||||
this.hydrateFormFromPlaylist()
|
||||
|
||||
this.serverService.videoPlaylistPrivaciesLoaded.subscribe(
|
||||
() => {
|
||||
this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies()
|
||||
.filter(p => {
|
||||
// If the playlist is not private, we cannot put it in private anymore
|
||||
return this.videoPlaylistToUpdate.privacy.id === VideoPlaylistPrivacy.PRIVATE ||
|
||||
p.id !== VideoPlaylistPrivacy.PRIVATE
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.paramsSub) this.paramsSub.unsubscribe()
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
this.error = undefined
|
||||
|
||||
const body = this.form.value
|
||||
const videoPlaylistUpdate: VideoPlaylistUpdate = {
|
||||
displayName: body['display-name'],
|
||||
privacy: body['privacy'],
|
||||
description: body.description || null,
|
||||
videoChannelId: body.videoChannelId || null,
|
||||
thumbnailfile: body.thumbnailfile || undefined
|
||||
}
|
||||
|
||||
this.videoPlaylistService.updateVideoPlaylist(this.videoPlaylistToUpdate, videoPlaylistUpdate).subscribe(
|
||||
() => {
|
||||
this.notifier.success(
|
||||
this.i18n('Playlist {{videoPlaylistName}} updated.', { videoPlaylistName: videoPlaylistUpdate.displayName })
|
||||
)
|
||||
|
||||
this.router.navigate([ '/my-account', 'video-playlists' ])
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
)
|
||||
}
|
||||
|
||||
isCreation () {
|
||||
return false
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
return this.i18n('Update')
|
||||
}
|
||||
|
||||
private hydrateFormFromPlaylist () {
|
||||
this.form.patchValue({
|
||||
'display-name': this.videoPlaylistToUpdate.displayName,
|
||||
privacy: this.videoPlaylistToUpdate.privacy.id,
|
||||
description: this.videoPlaylistToUpdate.description,
|
||||
videoChannelId: this.videoPlaylistToUpdate.videoChannel ? this.videoPlaylistToUpdate.videoChannel.id : null
|
||||
})
|
||||
|
||||
fetch(this.videoPlaylistToUpdate.thumbnailUrl)
|
||||
.then(response => response.blob())
|
||||
.then(data => {
|
||||
this.form.patchValue({
|
||||
thumbnailfile: data
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<div class="video-playlists-header">
|
||||
<a class="create-button" routerLink="create">
|
||||
<my-global-icon iconName="add"></my-global-icon>
|
||||
<ng-container i18n>Create a new playlist</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="video-playlists">
|
||||
<div *ngFor="let playlist of videoPlaylists" class="video-playlist">
|
||||
<div class="miniature-wrapper">
|
||||
<my-video-playlist-miniature [playlist]="playlist"></my-video-playlist-miniature>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons">
|
||||
<my-delete-button (click)="deleteVideoPlaylist(playlist)"></my-delete-button>
|
||||
|
||||
<my-edit-button [routerLink]="[ 'update', playlist.uuid ]"></my-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,50 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.create-button {
|
||||
@include create-button;
|
||||
}
|
||||
|
||||
/deep/ .action-button {
|
||||
&.action-button-delete {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-playlist {
|
||||
@include row-blocks;
|
||||
|
||||
.miniature-wrapper {
|
||||
flex-grow: 1;
|
||||
|
||||
/deep/ .miniature {
|
||||
display: flex;
|
||||
|
||||
.miniature-bottom {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-playlist-buttons {
|
||||
min-width: 190px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-playlists-header {
|
||||
text-align: right;
|
||||
margin: 20px 0 50px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.video-playlists-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.video-playlist {
|
||||
|
||||
.video-playlist-buttons {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { AuthService } from '../../core/auth'
|
||||
import { ConfirmService } from '../../core/confirm'
|
||||
import { User } from '@app/shared'
|
||||
import { flatMap } from 'rxjs/operators'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||
import { VideoPlaylistType } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-video-playlists',
|
||||
templateUrl: './my-account-video-playlists.component.html',
|
||||
styleUrls: [ './my-account-video-playlists.component.scss' ]
|
||||
})
|
||||
export class MyAccountVideoPlaylistsComponent implements OnInit {
|
||||
videoPlaylists: VideoPlaylist[] = []
|
||||
|
||||
pagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
totalItems: null
|
||||
}
|
||||
|
||||
private user: User
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private videoPlaylistService: VideoPlaylistService,
|
||||
private i18n: I18n
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.user = this.authService.getUser()
|
||||
|
||||
this.loadVideoPlaylists()
|
||||
}
|
||||
|
||||
async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) {
|
||||
const res = await this.confirmService.confirm(
|
||||
this.i18n(
|
||||
'Do you really want to delete {{playlistDisplayName}}?',
|
||||
{ playlistDisplayName: videoPlaylist.displayName }
|
||||
),
|
||||
this.i18n('Delete')
|
||||
)
|
||||
if (res === false) return
|
||||
|
||||
this.videoPlaylistService.removeVideoPlaylist(videoPlaylist)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.videoPlaylists = this.videoPlaylists
|
||||
.filter(p => p.id !== videoPlaylist.id)
|
||||
|
||||
this.notifier.success(
|
||||
this.i18n('Playlist {{playlistDisplayName}} deleted.', { playlistDisplayName: videoPlaylist.displayName })
|
||||
)
|
||||
},
|
||||
|
||||
error => this.notifier.error(error.message)
|
||||
)
|
||||
}
|
||||
|
||||
isRegularPlaylist (playlist: VideoPlaylist) {
|
||||
return playlist.type.id === VideoPlaylistType.REGULAR
|
||||
}
|
||||
|
||||
private loadVideoPlaylists () {
|
||||
this.authService.userInformationLoaded
|
||||
.pipe(flatMap(() => this.videoPlaylistService.listAccountPlaylists(this.user.account)))
|
||||
.subscribe(res => this.videoPlaylists = res.data)
|
||||
}
|
||||
|
||||
private ofNearOfBottom () {
|
||||
// Last page
|
||||
if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
|
||||
|
||||
this.pagination.currentPage += 1
|
||||
this.loadVideoPlaylists()
|
||||
}
|
||||
}
|
|
@ -27,6 +27,10 @@ export class MyAccountComponent {
|
|||
label: this.i18n('My videos'),
|
||||
routerLink: '/my-account/videos'
|
||||
},
|
||||
{
|
||||
label: this.i18n('My playlists'),
|
||||
routerLink: '/my-account/video-playlists'
|
||||
},
|
||||
{
|
||||
label: this.i18n('My subscriptions'),
|
||||
routerLink: '/my-account/subscriptions'
|
||||
|
|
|
@ -25,6 +25,13 @@ import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-b
|
|||
import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
|
||||
import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
|
||||
import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences'
|
||||
import {
|
||||
MyAccountVideoPlaylistCreateComponent
|
||||
} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component'
|
||||
import {
|
||||
MyAccountVideoPlaylistUpdateComponent
|
||||
} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component'
|
||||
import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -57,7 +64,11 @@ import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-a
|
|||
MyAccountServerBlocklistComponent,
|
||||
MyAccountHistoryComponent,
|
||||
MyAccountNotificationsComponent,
|
||||
MyAccountNotificationPreferencesComponent
|
||||
MyAccountNotificationPreferencesComponent,
|
||||
|
||||
MyAccountVideoPlaylistCreateComponent,
|
||||
MyAccountVideoPlaylistUpdateComponent,
|
||||
MyAccountVideoPlaylistsComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
|
|
@ -74,6 +74,7 @@ export class AppComponent implements OnInit {
|
|||
this.serverService.loadVideoLanguages()
|
||||
this.serverService.loadVideoLicences()
|
||||
this.serverService.loadVideoPrivacies()
|
||||
this.serverService.loadVideoPlaylistPrivacies()
|
||||
|
||||
// Do not display menu on small screens
|
||||
if (this.screenService.isInSmallView()) {
|
||||
|
|
|
@ -9,17 +9,20 @@ import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos
|
|||
import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n'
|
||||
import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
|
||||
import { sortBy } from '@app/shared/misc/utils'
|
||||
import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
|
||||
|
||||
@Injectable()
|
||||
export class ServerService {
|
||||
private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/'
|
||||
private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/'
|
||||
private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
|
||||
private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/'
|
||||
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
|
||||
private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
|
||||
|
||||
configLoaded = new ReplaySubject<boolean>(1)
|
||||
videoPrivaciesLoaded = new ReplaySubject<boolean>(1)
|
||||
videoPlaylistPrivaciesLoaded = new ReplaySubject<boolean>(1)
|
||||
videoCategoriesLoaded = new ReplaySubject<boolean>(1)
|
||||
videoLicencesLoaded = new ReplaySubject<boolean>(1)
|
||||
videoLanguagesLoaded = new ReplaySubject<boolean>(1)
|
||||
|
@ -101,6 +104,7 @@ export class ServerService {
|
|||
private videoLicences: Array<VideoConstant<number>> = []
|
||||
private videoLanguages: Array<VideoConstant<string>> = []
|
||||
private videoPrivacies: Array<VideoConstant<VideoPrivacy>> = []
|
||||
private videoPlaylistPrivacies: Array<VideoConstant<VideoPlaylistPrivacy>> = []
|
||||
|
||||
constructor (
|
||||
private http: HttpClient,
|
||||
|
@ -121,19 +125,28 @@ export class ServerService {
|
|||
}
|
||||
|
||||
loadVideoCategories () {
|
||||
return this.loadVideoAttributeEnum('categories', this.videoCategories, this.videoCategoriesLoaded, true)
|
||||
return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'categories', this.videoCategories, this.videoCategoriesLoaded, true)
|
||||
}
|
||||
|
||||
loadVideoLicences () {
|
||||
return this.loadVideoAttributeEnum('licences', this.videoLicences, this.videoLicencesLoaded)
|
||||
return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'licences', this.videoLicences, this.videoLicencesLoaded)
|
||||
}
|
||||
|
||||
loadVideoLanguages () {
|
||||
return this.loadVideoAttributeEnum('languages', this.videoLanguages, this.videoLanguagesLoaded, true)
|
||||
return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'languages', this.videoLanguages, this.videoLanguagesLoaded, true)
|
||||
}
|
||||
|
||||
loadVideoPrivacies () {
|
||||
return this.loadVideoAttributeEnum('privacies', this.videoPrivacies, this.videoPrivaciesLoaded)
|
||||
return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'privacies', this.videoPrivacies, this.videoPrivaciesLoaded)
|
||||
}
|
||||
|
||||
loadVideoPlaylistPrivacies () {
|
||||
return this.loadAttributeEnum(
|
||||
ServerService.BASE_VIDEO_PLAYLIST_URL,
|
||||
'privacies',
|
||||
this.videoPlaylistPrivacies,
|
||||
this.videoPlaylistPrivaciesLoaded
|
||||
)
|
||||
}
|
||||
|
||||
getConfig () {
|
||||
|
@ -156,7 +169,12 @@ export class ServerService {
|
|||
return this.videoPrivacies
|
||||
}
|
||||
|
||||
private loadVideoAttributeEnum (
|
||||
getVideoPlaylistPrivacies () {
|
||||
return this.videoPlaylistPrivacies
|
||||
}
|
||||
|
||||
private loadAttributeEnum (
|
||||
baseUrl: string,
|
||||
attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
|
||||
hashToPopulate: VideoConstant<string | number>[],
|
||||
notifier: ReplaySubject<boolean>,
|
||||
|
@ -165,7 +183,7 @@ export class ServerService {
|
|||
this.localeObservable
|
||||
.pipe(
|
||||
switchMap(translations => {
|
||||
return this.http.get<{ [id: string]: string }>(ServerService.BASE_VIDEO_URL + attributeName)
|
||||
return this.http.get<{ [id: string]: string }>(baseUrl + attributeName)
|
||||
.pipe(map(data => ({ data, translations })))
|
||||
})
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { GlobalIconName } from '@app/shared/icons/global-icon.component'
|
||||
import { GlobalIconName } from '@app/shared/images/global-icon.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-button',
|
||||
|
|
|
@ -10,6 +10,7 @@ export * from './video-blacklist-validators.service'
|
|||
export * from './video-channel-validators.service'
|
||||
export * from './video-comment-validators.service'
|
||||
export * from './video-validators.service'
|
||||
export * from './video-playlist-validators.service'
|
||||
export * from './video-captions-validators.service'
|
||||
export * from './video-change-ownership-validators.service'
|
||||
export * from './video-accept-ownership-validators.service'
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Validators } from '@angular/forms'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BuildFormValidator } from '@app/shared'
|
||||
|
||||
@Injectable()
|
||||
export class VideoPlaylistValidatorsService {
|
||||
readonly VIDEO_PLAYLIST_DISPLAY_NAME: BuildFormValidator
|
||||
readonly VIDEO_PLAYLIST_PRIVACY: BuildFormValidator
|
||||
readonly VIDEO_PLAYLIST_DESCRIPTION: BuildFormValidator
|
||||
readonly VIDEO_PLAYLIST_CHANNEL_ID: BuildFormValidator
|
||||
|
||||
constructor (private i18n: I18n) {
|
||||
this.VIDEO_PLAYLIST_DISPLAY_NAME = {
|
||||
VALIDATORS: [
|
||||
Validators.required,
|
||||
Validators.minLength(1),
|
||||
Validators.maxLength(120)
|
||||
],
|
||||
MESSAGES: {
|
||||
'required': this.i18n('Display name is required.'),
|
||||
'minlength': this.i18n('Display name must be at least 1 character long.'),
|
||||
'maxlength': this.i18n('Display name cannot be more than 120 characters long.')
|
||||
}
|
||||
}
|
||||
|
||||
this.VIDEO_PLAYLIST_PRIVACY = {
|
||||
VALIDATORS: [
|
||||
Validators.required
|
||||
],
|
||||
MESSAGES: {
|
||||
'required': this.i18n('Privacy is required.')
|
||||
}
|
||||
}
|
||||
|
||||
this.VIDEO_PLAYLIST_DESCRIPTION = {
|
||||
VALIDATORS: [
|
||||
Validators.minLength(3),
|
||||
Validators.maxLength(1000)
|
||||
],
|
||||
MESSAGES: {
|
||||
'minlength': i18n('Description must be at least 3 characters long.'),
|
||||
'maxlength': i18n('Description cannot be more than 1000 characters long.')
|
||||
}
|
||||
}
|
||||
|
||||
this.VIDEO_PLAYLIST_CHANNEL_ID = {
|
||||
VALIDATORS: [ ],
|
||||
MESSAGES: { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,18 +4,18 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
|
|||
import { ServerService } from '@app/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-image',
|
||||
styleUrls: [ './video-image.component.scss' ],
|
||||
templateUrl: './video-image.component.html',
|
||||
selector: 'my-image-upload',
|
||||
styleUrls: [ './image-upload.component.scss' ],
|
||||
templateUrl: './image-upload.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => VideoImageComponent),
|
||||
useExisting: forwardRef(() => ImageUploadComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class VideoImageComponent implements ControlValueAccessor {
|
||||
export class ImageUploadComponent implements ControlValueAccessor {
|
||||
@Input() inputLabel: string
|
||||
@Input() inputName: string
|
||||
@Input() previewWidth: string
|
|
@ -17,7 +17,7 @@ function getParameterByName (name: string, url: string) {
|
|||
return decodeURIComponent(results[2].replace(/\+/g, ' '))
|
||||
}
|
||||
|
||||
function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support: string }[]) {
|
||||
function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support?: string }[]) {
|
||||
return new Promise(res => {
|
||||
authService.userInformationLoaded
|
||||
.subscribe(
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
VideoChangeOwnershipValidatorsService,
|
||||
VideoChannelValidatorsService,
|
||||
VideoCommentValidatorsService,
|
||||
VideoPlaylistValidatorsService,
|
||||
VideoValidatorsService
|
||||
} from '@app/shared/forms'
|
||||
import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
|
||||
|
@ -68,8 +69,11 @@ import { UserNotificationsComponent } from '@app/shared/users/user-notifications
|
|||
import { InstanceService } from '@app/shared/instance/instance.service'
|
||||
import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer'
|
||||
import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
|
||||
import { GlobalIconComponent } from '@app/shared/icons/global-icon.component'
|
||||
import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
|
||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||
import { ImageUploadComponent } from '@app/shared/images/image-upload.component'
|
||||
import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
|
||||
import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -92,8 +96,11 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
|
|||
declarations: [
|
||||
LoaderComponent,
|
||||
SmallLoaderComponent,
|
||||
|
||||
VideoThumbnailComponent,
|
||||
VideoMiniatureComponent,
|
||||
VideoPlaylistMiniatureComponent,
|
||||
|
||||
FeedComponent,
|
||||
ButtonComponent,
|
||||
DeleteButtonComponent,
|
||||
|
@ -116,7 +123,9 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
|
|||
TopMenuDropdownComponent,
|
||||
UserNotificationsComponent,
|
||||
ConfirmComponent,
|
||||
GlobalIconComponent
|
||||
|
||||
GlobalIconComponent,
|
||||
ImageUploadComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -138,8 +147,11 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
|
|||
|
||||
LoaderComponent,
|
||||
SmallLoaderComponent,
|
||||
|
||||
VideoThumbnailComponent,
|
||||
VideoMiniatureComponent,
|
||||
VideoPlaylistMiniatureComponent,
|
||||
|
||||
FeedComponent,
|
||||
ButtonComponent,
|
||||
DeleteButtonComponent,
|
||||
|
@ -159,7 +171,9 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
|
|||
TopMenuDropdownComponent,
|
||||
UserNotificationsComponent,
|
||||
ConfirmComponent,
|
||||
|
||||
GlobalIconComponent,
|
||||
ImageUploadComponent,
|
||||
|
||||
NumberFormatterPipe,
|
||||
ObjectLengthPipe,
|
||||
|
@ -177,6 +191,7 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
|
|||
VideoService,
|
||||
AccountService,
|
||||
VideoChannelService,
|
||||
VideoPlaylistService,
|
||||
VideoCaptionService,
|
||||
VideoImportService,
|
||||
UserSubscriptionService,
|
||||
|
@ -186,6 +201,7 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
|
|||
LoginValidatorsService,
|
||||
ResetPasswordValidatorsService,
|
||||
UserValidatorsService,
|
||||
VideoPlaylistValidatorsService,
|
||||
VideoAbuseValidatorsService,
|
||||
VideoChannelValidatorsService,
|
||||
VideoCommentValidatorsService,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<div class="miniature">
|
||||
<a
|
||||
[routerLink]="[ '/videos/watch' ]" [attr.title]="playlist.displayName"
|
||||
class="miniature-thumbnail"
|
||||
>
|
||||
<img alt="" [attr.aria-labelledby]="playlist.displayName" [attr.src]="playlist.thumbnailUrl" />
|
||||
|
||||
<div class="miniature-playlist-info-overlay">
|
||||
<ng-container i18n>{playlist.videosLength, plural, =0 {No videos} =1 {1 video} other {{{playlist.videosLength}} videos}}</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="play-overlay">
|
||||
<div class="icon"></div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="miniature-bottom">
|
||||
<a tabindex="-1" class="miniature-name" [routerLink]="[ '/videos/watch' ]" [attr.title]="playlist.displayName">
|
||||
{{ playlist.displayName }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,34 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import '_miniature';
|
||||
|
||||
.miniature {
|
||||
display: inline-block;
|
||||
|
||||
.miniature-thumbnail {
|
||||
@include miniature-thumbnail;
|
||||
|
||||
.miniature-playlist-info-overlay {
|
||||
@include static-thumbnail-overlay;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: $video-thumbnail-height;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.miniature-bottom {
|
||||
width: 200px;
|
||||
margin-top: 2px;
|
||||
line-height: normal;
|
||||
|
||||
.miniature-name {
|
||||
@include miniature-name;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-playlist-miniature',
|
||||
styleUrls: [ './video-playlist-miniature.component.scss' ],
|
||||
templateUrl: './video-playlist-miniature.component.html'
|
||||
})
|
||||
export class VideoPlaylistMiniatureComponent {
|
||||
@Input() playlist: VideoPlaylist
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import {
|
||||
VideoChannelSummary,
|
||||
VideoConstant,
|
||||
VideoPlaylist as ServerVideoPlaylist,
|
||||
VideoPlaylistPrivacy,
|
||||
VideoPlaylistType
|
||||
} from '../../../../../shared/models/videos'
|
||||
import { AccountSummary, peertubeTranslate } from '@shared/models'
|
||||
import { Actor } from '@app/shared/actor/actor.model'
|
||||
import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
|
||||
|
||||
export class VideoPlaylist implements ServerVideoPlaylist {
|
||||
id: number
|
||||
uuid: string
|
||||
isLocal: boolean
|
||||
|
||||
displayName: string
|
||||
description: string
|
||||
privacy: VideoConstant<VideoPlaylistPrivacy>
|
||||
|
||||
thumbnailPath: string
|
||||
|
||||
videosLength: number
|
||||
|
||||
type: VideoConstant<VideoPlaylistType>
|
||||
|
||||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
|
||||
ownerAccount: AccountSummary
|
||||
videoChannel?: VideoChannelSummary
|
||||
|
||||
thumbnailUrl: string
|
||||
|
||||
ownerBy: string
|
||||
ownerAvatarUrl: string
|
||||
|
||||
videoChannelBy?: string
|
||||
videoChannelAvatarUrl?: string
|
||||
|
||||
constructor (hash: ServerVideoPlaylist, translations: {}) {
|
||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||
|
||||
this.id = hash.id
|
||||
this.uuid = hash.uuid
|
||||
this.isLocal = hash.isLocal
|
||||
|
||||
this.displayName = hash.displayName
|
||||
this.description = hash.description
|
||||
this.privacy = hash.privacy
|
||||
|
||||
this.thumbnailPath = hash.thumbnailPath
|
||||
this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
|
||||
|
||||
this.videosLength = hash.videosLength
|
||||
|
||||
this.type = hash.type
|
||||
|
||||
this.createdAt = new Date(hash.createdAt)
|
||||
this.updatedAt = new Date(hash.updatedAt)
|
||||
|
||||
this.ownerAccount = hash.ownerAccount
|
||||
this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
|
||||
this.ownerAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.ownerAccount)
|
||||
|
||||
if (hash.videoChannel) {
|
||||
this.videoChannel = hash.videoChannel
|
||||
this.videoChannelBy = Actor.CREATE_BY_STRING(hash.videoChannel.name, hash.videoChannel.host)
|
||||
this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.videoChannel)
|
||||
}
|
||||
|
||||
this.privacy.label = peertubeTranslate(this.privacy.label, translations)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
import { catchError, map, switchMap } from 'rxjs/operators'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Observable } from 'rxjs'
|
||||
import { RestExtractor } from '../rest/rest-extractor.service'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { ResultList } from '../../../../../shared'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { VideoPlaylist as VideoPlaylistServerModel } from '@shared/models/videos/playlist/video-playlist.model'
|
||||
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
|
||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
|
||||
import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model'
|
||||
import { objectToFormData } from '@app/shared/misc/utils'
|
||||
import { ServerService } from '@app/core'
|
||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||
import { AccountService } from '@app/shared/account/account.service'
|
||||
import { Account } from '@app/shared/account/account.model'
|
||||
|
||||
@Injectable()
|
||||
export class VideoPlaylistService {
|
||||
static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/'
|
||||
|
||||
constructor (
|
||||
private authHttp: HttpClient,
|
||||
private serverService: ServerService,
|
||||
private restExtractor: RestExtractor
|
||||
) { }
|
||||
|
||||
listChannelPlaylists (videoChannel: VideoChannel): Observable<ResultList<VideoPlaylist>> {
|
||||
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/video-playlists'
|
||||
|
||||
return this.authHttp.get<ResultList<VideoPlaylist>>(url)
|
||||
.pipe(
|
||||
switchMap(res => this.extractPlaylists(res)),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
listAccountPlaylists (account: Account): Observable<ResultList<VideoPlaylist>> {
|
||||
const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists'
|
||||
|
||||
return this.authHttp.get<ResultList<VideoPlaylist>>(url)
|
||||
.pipe(
|
||||
switchMap(res => this.extractPlaylists(res)),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
getVideoPlaylist (id: string | number) {
|
||||
const url = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + id
|
||||
|
||||
return this.authHttp.get<VideoPlaylist>(url)
|
||||
.pipe(
|
||||
switchMap(res => this.extractPlaylist(res)),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
createVideoPlaylist (body: VideoPlaylistCreate) {
|
||||
const data = objectToFormData(body)
|
||||
|
||||
return this.authHttp.post(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL, data)
|
||||
.pipe(
|
||||
map(this.restExtractor.extractDataBool),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
updateVideoPlaylist (videoPlaylist: VideoPlaylist, body: VideoPlaylistUpdate) {
|
||||
const data = objectToFormData(body)
|
||||
|
||||
return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id, data)
|
||||
.pipe(
|
||||
map(this.restExtractor.extractDataBool),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
removeVideoPlaylist (videoPlaylist: VideoPlaylist) {
|
||||
return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id)
|
||||
.pipe(
|
||||
map(this.restExtractor.extractDataBool),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
extractPlaylists (result: ResultList<VideoPlaylistServerModel>) {
|
||||
return this.serverService.localeObservable
|
||||
.pipe(
|
||||
map(translations => {
|
||||
const playlistsJSON = result.data
|
||||
const total = result.total
|
||||
const playlists: VideoPlaylist[] = []
|
||||
|
||||
for (const playlistJSON of playlistsJSON) {
|
||||
playlists.push(new VideoPlaylist(playlistJSON, translations))
|
||||
}
|
||||
|
||||
return { data: playlists, total }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
extractPlaylist (playlist: VideoPlaylistServerModel) {
|
||||
return this.serverService.localeObservable
|
||||
.pipe(map(translations => new VideoPlaylist(playlist, translations)))
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
@import '_mixins';
|
||||
@import '_miniature';
|
||||
|
||||
.videos {
|
||||
text-align: center;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import '_miniature';
|
||||
|
||||
.video-miniature {
|
||||
display: inline-block;
|
||||
|
@ -14,26 +15,7 @@
|
|||
line-height: normal;
|
||||
|
||||
.video-miniature-name {
|
||||
@include ellipsis-multiline(
|
||||
$font-size: 1rem,
|
||||
$line-height: 1,
|
||||
$lines-to-show: 2
|
||||
);
|
||||
transition: color 0.2s;
|
||||
font-size: 16px;
|
||||
font-weight: $font-semibold;
|
||||
color: var(--mainForegroundColor);
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.blur-filter {
|
||||
filter: blur(3px);
|
||||
padding-left: 4px;
|
||||
}
|
||||
@include miniature-name;
|
||||
}
|
||||
|
||||
.video-miniature-created-at-views {
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
>
|
||||
<img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
|
||||
|
||||
<div class="video-thumbnail-overlay">{{ video.durationLabel }}</div>
|
||||
<div class="video-thumbnail-duration-overlay">{{ video.durationLabel }}</div>
|
||||
|
||||
<div class="play-overlay"></div>
|
||||
<div class="play-overlay">
|
||||
<div class="icon"></div>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar" *ngIf="video.userHistory?.currentTime">
|
||||
<div [ngStyle]="{ 'width.%': getProgressPercent() }"></div>
|
||||
|
|
|
@ -1,66 +1,9 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
$play-overlay-transition: 0.2s ease;
|
||||
$play-overlay-height: 26px;
|
||||
$play-overlay-width: 18px;
|
||||
@import '_miniature';
|
||||
|
||||
.video-thumbnail {
|
||||
@include disable-outline;
|
||||
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
width: $video-thumbnail-width;
|
||||
height: $video-thumbnail-height;
|
||||
background-color: #ececec;
|
||||
transition: filter $play-overlay-transition;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none !important;
|
||||
|
||||
filter: brightness(85%);
|
||||
|
||||
.play-overlay {
|
||||
opacity: 1;
|
||||
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
&.focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--mainColor);
|
||||
}
|
||||
|
||||
img {
|
||||
width: $video-thumbnail-width;
|
||||
height: $video-thumbnail-height;
|
||||
|
||||
&.blur-filter {
|
||||
filter: blur(5px);
|
||||
transform : scale(1.03);
|
||||
}
|
||||
}
|
||||
|
||||
.play-overlay {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.5);
|
||||
|
||||
transition: all $play-overlay-transition;
|
||||
|
||||
border-top: ($play-overlay-height / 2) solid transparent;
|
||||
border-bottom: ($play-overlay-height / 2) solid transparent;
|
||||
|
||||
border-left: $play-overlay-width solid rgba(255, 255, 255, 0.95);
|
||||
|
||||
opacity: 0;
|
||||
}
|
||||
@include miniature-thumbnail;
|
||||
|
||||
.progress-bar {
|
||||
height: 3px;
|
||||
|
@ -75,16 +18,15 @@ $play-overlay-width: 18px;
|
|||
}
|
||||
}
|
||||
|
||||
.video-thumbnail-overlay {
|
||||
.video-thumbnail-duration-overlay {
|
||||
@include static-thumbnail-overlay;
|
||||
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
display: inline-block;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: $font-bold;
|
||||
border-radius: 3px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,9 +117,8 @@ export class Video implements VideoServerModel {
|
|||
this.privacy.label = peertubeTranslate(this.privacy.label, translations)
|
||||
|
||||
this.scheduledUpdate = hash.scheduledUpdate
|
||||
this.originallyPublishedAt = hash.originallyPublishedAt ?
|
||||
new Date(hash.originallyPublishedAt.toString())
|
||||
: null
|
||||
this.originallyPublishedAt = hash.originallyPublishedAt ? new Date(hash.originallyPublishedAt.toString()) : null
|
||||
|
||||
if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
|
||||
|
||||
this.blacklisted = hash.blacklisted
|
||||
|
|
|
@ -188,17 +188,17 @@
|
|||
<div class="row advanced-settings">
|
||||
<div class="col-md-12 col-xl-8">
|
||||
<div class="form-group">
|
||||
<my-video-image
|
||||
<my-image-upload
|
||||
i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
|
||||
previewWidth="200px" previewHeight="110px"
|
||||
></my-video-image>
|
||||
></my-image-upload>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-video-image
|
||||
<my-image-upload
|
||||
i18n-inputLabel inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile"
|
||||
previewWidth="360px" previewHeight="200px"
|
||||
></my-video-image>
|
||||
></my-image-upload>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'
|
|||
import { TagInputModule } from 'ngx-chips'
|
||||
import { SharedModule } from '../../../shared/'
|
||||
import { VideoEditComponent } from './video-edit.component'
|
||||
import { VideoImageComponent } from './video-image.component'
|
||||
import { CalendarModule } from 'primeng/components/calendar/calendar'
|
||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||
|
||||
|
@ -16,7 +15,6 @@ import { VideoCaptionAddModalComponent } from './video-caption-add-modal.compone
|
|||
|
||||
declarations: [
|
||||
VideoEditComponent,
|
||||
VideoImageComponent,
|
||||
VideoCaptionAddModalComponent
|
||||
],
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import '_miniature';
|
||||
|
||||
.section {
|
||||
padding-top: 10px;
|
||||
|
@ -50,4 +51,4 @@
|
|||
.section {
|
||||
@include video-miniature-small-screen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
@mixin miniature-name {
|
||||
@include ellipsis-multiline(
|
||||
$font-size: 1rem,
|
||||
$line-height: 1,
|
||||
$lines-to-show: 2
|
||||
);
|
||||
transition: color 0.2s;
|
||||
font-size: 16px;
|
||||
font-weight: $font-semibold;
|
||||
color: var(--mainForegroundColor);
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.blur-filter {
|
||||
filter: blur(3px);
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
$play-overlay-transition: 0.2s ease;
|
||||
$play-overlay-height: 26px;
|
||||
$play-overlay-width: 18px;
|
||||
|
||||
@mixin miniature-thumbnail {
|
||||
@include disable-outline;
|
||||
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
width: $video-thumbnail-width;
|
||||
height: $video-thumbnail-height;
|
||||
background-color: #ececec;
|
||||
transition: filter $play-overlay-transition;
|
||||
|
||||
.play-overlay {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: $video-thumbnail-width;
|
||||
height: $video-thumbnail-height;
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
|
||||
&, .icon {
|
||||
transition: all $play-overlay-transition;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.5);
|
||||
|
||||
border-top: ($play-overlay-height / 2) solid transparent;
|
||||
border-bottom: ($play-overlay-height / 2) solid transparent;
|
||||
|
||||
border-left: $play-overlay-width solid rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none !important;
|
||||
|
||||
.play-overlay {
|
||||
opacity: 1;
|
||||
|
||||
.icon {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--mainColor);
|
||||
}
|
||||
|
||||
img {
|
||||
width: $video-thumbnail-width;
|
||||
height: $video-thumbnail-height;
|
||||
|
||||
&.blur-filter {
|
||||
filter: blur(5px);
|
||||
transform : scale(1.03);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin static-thumbnail-overlay {
|
||||
display: inline-block;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@mixin video-miniature-small-screen {
|
||||
text-align: center;
|
||||
|
||||
/deep/ .video-miniature {
|
||||
padding-right: 0;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.video-miniature-information {
|
||||
width: 100% !important;
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.video-thumbnail {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -516,31 +516,3 @@
|
|||
}
|
||||
}
|
||||
|
||||
@mixin video-miniature-small-screen {
|
||||
text-align: center;
|
||||
|
||||
/deep/ .video-miniature {
|
||||
padding-right: 0;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.video-miniature-information {
|
||||
width: 100% !important;
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.video-thumbnail {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@app/*": [ "app/*" ],
|
||||
"@shared/*": [ "../../shared/*" ],
|
||||
"video.js": [ "../node_modules/video.js/dist/alt/video.core.js" ],
|
||||
"fs": [ "./shims/noop" ],
|
||||
"http": [ "./shims/http" ],
|
||||
|
@ -41,11 +42,14 @@
|
|||
"strictInjectionParameters": true,
|
||||
"fullTemplateTypeCheck": true
|
||||
},
|
||||
"include": [
|
||||
"../../shared"
|
||||
],
|
||||
"exclude": [
|
||||
"../../node_modules",
|
||||
"../node_modules",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"../server",
|
||||
"src/**/*.spec.ts"
|
||||
"../dist",
|
||||
"../../server",
|
||||
"../src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
buildLanguages,
|
||||
VIDEO_CATEGORIES,
|
||||
VIDEO_IMPORT_STATES,
|
||||
VIDEO_LICENCES,
|
||||
VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES,
|
||||
VIDEO_PRIVACIES,
|
||||
VIDEO_STATES
|
||||
} from '../../server/initializers/constants'
|
||||
|
@ -46,6 +46,8 @@ values(VIDEO_CATEGORIES)
|
|||
.concat(values(VIDEO_PRIVACIES))
|
||||
.concat(values(VIDEO_STATES))
|
||||
.concat(values(VIDEO_IMPORT_STATES))
|
||||
.concat(values(VIDEO_PLAYLIST_PRIVACIES))
|
||||
.concat(values(VIDEO_PLAYLIST_TYPES))
|
||||
.concat([
|
||||
'This video does not exist.',
|
||||
'We cannot fetch the video. Please try again later.',
|
||||
|
|
|
@ -642,7 +642,7 @@ let STATIC_MAX_AGE = '2h'
|
|||
// Videos thumbnail size
|
||||
const THUMBNAILS_SIZE = {
|
||||
width: 223,
|
||||
height: 112
|
||||
height: 122
|
||||
}
|
||||
const PREVIEWS_SIZE = {
|
||||
width: 560,
|
||||
|
|
|
@ -344,6 +344,7 @@ function getCommonPlaylistEditAttributes () {
|
|||
.custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
|
||||
body('videoChannelId')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.toInt()
|
||||
] as (ValidationChain | express.Handler)[]
|
||||
}
|
||||
|
|
|
@ -11,6 +11,13 @@ export * from './blacklist/video-blacklist-update.model'
|
|||
export * from './channel/video-channel-create.model'
|
||||
export * from './channel/video-channel-update.model'
|
||||
export * from './channel/video-channel.model'
|
||||
export * from './playlist/video-playlist-create.model'
|
||||
export * from './playlist/video-playlist-element-create.model'
|
||||
export * from './playlist/video-playlist-element-update.model'
|
||||
export * from './playlist/video-playlist-privacy.model'
|
||||
export * from './playlist/video-playlist-type.model'
|
||||
export * from './playlist/video-playlist-update.model'
|
||||
export * from './playlist/video-playlist.model'
|
||||
export * from './video-change-ownership.model'
|
||||
export * from './video-change-ownership-create.model'
|
||||
export * from './video-create.model'
|
||||
|
@ -27,4 +34,4 @@ export * from './caption/video-caption-update.model'
|
|||
export * from './import/video-import-create.model'
|
||||
export * from './import/video-import-state.enum'
|
||||
export * from './import/video-import.model'
|
||||
export { VideoConstant } from './video-constant.model'
|
||||
export * from './video-constant.model'
|
||||
|
|
|
@ -21,6 +21,6 @@ export interface VideoPlaylist {
|
|||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
|
||||
ownerAccount?: AccountSummary
|
||||
ownerAccount: AccountSummary
|
||||
videoChannel?: VideoChannelSummary
|
||||
}
|
||||
|
|
|
@ -265,9 +265,21 @@ async function checkPlaylistFilesWereRemoved (
|
|||
}
|
||||
}
|
||||
|
||||
function getVideoPlaylistPrivacies (url: string) {
|
||||
const path = '/api/v1/video-playlists/privacies'
|
||||
|
||||
return makeGetRequest({
|
||||
url,
|
||||
path,
|
||||
statusCodeExpected: 200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
getVideoPlaylistPrivacies,
|
||||
|
||||
getVideoPlaylistsList,
|
||||
getVideoChannelPlaylistsList,
|
||||
getAccountPlaylistsList,
|
||||
|
|
Loading…
Reference in New Issue