}
},
{
- path: 'video-playlists/:videoPlaylistId',
- component: MyAccountVideoPlaylistElementsComponent,
+ path: 'video-playlists/create',
+ component: MyAccountVideoPlaylistCreateComponent,
data: {
meta: {
- title: 'Playlist elements'
+ title: 'Create new playlist'
}
}
},
{
- path: 'video-playlists/create',
- component: MyAccountVideoPlaylistCreateComponent,
+ path: 'video-playlists/:videoPlaylistId',
+ component: MyAccountVideoPlaylistElementsComponent,
data: {
meta: {
- title: 'Create new playlist'
+ title: 'Playlist elements'
}
}
},
-<div i18n class="no-results" *ngIf="pagination.totalItems === 0">No videos in this playlist.</div>
+<div class="row">
-<div
- class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"
- cdkDropList (cdkDropListDropped)="drop($event)"
->
- <div class="video" *ngFor="let video of videos" cdkDrag (cdkDragMoved)="onDragMove($event)">
- <my-video-playlist-element-miniature [video]="video" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)">
- </my-video-playlist-element-miniature>
+ <div class="playlist-info col-xs-12 col-md-5 col-xl-3">
+ <my-video-playlist-miniature
+ *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true"
+ [displayDescription]="true" [displayPrivacy]="true"
+ ></my-video-playlist-miniature>
+ </div>
+
+ <div class="col-xs-12 col-md-7 col-xl-9">
+ <div i18n class="no-results" *ngIf="pagination.totalItems === 0">No videos in this playlist.</div>
+
+ <div
+ class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"
+ cdkDropList (cdkDropListDropped)="drop($event)"
+ >
+ <div class="video" *ngFor="let video of videos; trackBy: trackByFn" cdkDrag (cdkDragMoved)="onDragMove($event)">
+ <my-video-playlist-element-miniature
+ [video]="video" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)"
+ [position]="video.playlistElement.position"
+ >
+ </my-video-playlist-element-miniature>
+ </div>
+ </div>
</div>
</div>
@import '_mixins';
@import '_miniature';
+.playlist-info {
+ background-color: var(--submenuColor);
+ margin-left: -15px;
+ margin-top: -$sub-menu-margin-bottom;
+
+ padding: $sub-menu-margin-bottom 0;
+
+ display: flex;
+ justify-content: center;
+}
+
// Thanks Angular CDK <3 https://material.angular.io/cdk/drag-drop/examples
.cdk-drag-preview {
box-sizing: border-box;
pagination: ComponentPagination = {
currentPage: 1,
- itemsPerPage: 10,
+ itemsPerPage: 30,
totalItems: null
}
this.loadElements()
}
+ trackByFn (index: number, elem: Video) {
+ return elem.id
+ }
+
private loadElements () {
this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
.subscribe(({ totalVideos, videos }) => {
<div class="video-playlists" myInfiniteScroller (nearOfBottom)="onNearOfBottom()">
<div *ngFor="let playlist of videoPlaylists" class="video-playlist">
<div class="miniature-wrapper">
- <my-video-playlist-miniature [playlist]="playlist" [toManage]="true"></my-video-playlist-miniature>
+ <my-video-playlist-miniature [playlist]="playlist" [toManage]="true" [displayChannel]="true" [displayDescription]="true" [displayPrivacy]="true"
+ ></my-video-playlist-miniature>
</div>
<div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons">
/deep/ .miniature {
display: flex;
- .miniature-bottom {
+ .miniature-info {
margin-left: 10px;
+ width: auto;
}
}
}
<div class="video-info">
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
<span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
- <div class="video-info-private">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
+ <div class="video-info-privacy">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
<div *ngIf="video.blacklisted" class="video-info-blacklisted">
<span class="blacklisted-label" i18n>Blacklisted</span>
<span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
}
.video-info-date-views,
- .video-info-private,
+ .video-info-privacy,
.video-info-blacklisted {
font-size: 13px;
- &.video-info-private,
+ &.video-info-privacy,
&.video-info-blacklisted .blacklisted-label {
font-weight: $font-semibold;
}
--- /dev/null
+<div i18n class="title-page title-page-single">
+ Created {{pagination.totalItems}} playlists
+</div>
+
+<div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div>
+
+<div class="video-playlist" myInfiniteScroller (nearOfBottom)="onNearOfBottom()">
+ <div *ngFor="let playlist of videoPlaylists">
+ <my-video-playlist-miniature [playlist]="playlist" [toManage]="false"></my-video-playlist-miniature>
+ </div>
+</div>
--- /dev/null
+.video-playlist {
+ display: flex;
+ justify-content: center;
+
+ my-video-playlist-miniature {
+ margin-right: 15px;
+ margin-bottom: 30px;
+ }
+}
--- /dev/null
+import { Component, OnDestroy, OnInit } from '@angular/core'
+import { AuthService } from '../../core/auth'
+import { ConfirmService } from '../../core/confirm'
+import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
+import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
+import { flatMap } from 'rxjs/operators'
+import { Subscription } from 'rxjs'
+import { Notifier } from '@app/core'
+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'
+
+@Component({
+ selector: 'my-video-channel-playlists',
+ templateUrl: './video-channel-playlists.component.html',
+ styleUrls: [ './video-channel-playlists.component.scss' ]
+})
+export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy {
+ videoPlaylists: VideoPlaylist[] = []
+
+ pagination: ComponentPagination = {
+ currentPage: 1,
+ itemsPerPage: 20,
+ totalItems: null
+ }
+
+ private videoChannelSub: Subscription
+ private videoChannel: VideoChannel
+
+ constructor (
+ private authService: AuthService,
+ private notifier: Notifier,
+ private confirmService: ConfirmService,
+ private videoPlaylistService: VideoPlaylistService,
+ private videoChannelService: VideoChannelService
+ ) {}
+
+ ngOnInit () {
+ // Parent get the video channel for us
+ this.videoChannelSub = this.videoChannelService.videoChannelLoaded
+ .subscribe(videoChannel => {
+ this.videoChannel = videoChannel
+ this.loadVideoPlaylists()
+ })
+ }
+
+ ngOnDestroy () {
+ if (this.videoChannelSub) this.videoChannelSub.unsubscribe()
+ }
+
+ onNearOfBottom () {
+ // Last page
+ if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
+
+ this.pagination.currentPage += 1
+ this.loadVideoPlaylists()
+ }
+
+ private loadVideoPlaylists () {
+ this.authService.userInformationLoaded
+ .pipe(flatMap(() => this.videoPlaylistService.listChannelPlaylists(this.videoChannel)))
+ .subscribe(res => {
+ this.videoPlaylists = this.videoPlaylists.concat(res.data)
+ this.pagination.totalItems = res.total
+ })
+ }
+}
import { VideoChannelsComponent } from './video-channels.component'
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component'
+import { VideoChannelPlaylistsComponent } from '@app/+video-channels/video-channel-playlists/video-channel-playlists.component'
const videoChannelsRoutes: Routes = [
{
}
}
},
+ {
+ path: 'video-playlists',
+ component: VideoChannelPlaylistsComponent,
+ data: {
+ meta: {
+ title: 'Video channel playlists'
+ }
+ }
+ },
{
path: 'about',
component: VideoChannelAboutComponent,
<div class="links">
<a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
+ <a i18n routerLink="video-playlists" routerLinkActive="active" class="title-page">Video playlists</a>
<a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a>
</div>
</div>
import { VideoChannelsComponent } from './video-channels.component'
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component'
+import { VideoChannelPlaylistsComponent } from '@app/+video-channels/video-channel-playlists/video-channel-playlists.component'
@NgModule({
imports: [
declarations: [
VideoChannelsComponent,
VideoChannelVideosComponent,
- VideoChannelAboutComponent
+ VideoChannelAboutComponent,
+ VideoChannelPlaylistsComponent
],
exports: [
<a [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()">
<div class="position">
<my-global-icon *ngIf="playing" iconName="play"></my-global-icon>
- <ng-container *ngIf="!playing">{{ video.playlistElement.position }}</ng-container>
+ <ng-container *ngIf="!playing">{{ position }}</ng-container>
</div>
<my-video-thumbnail
font-weight: $font-semibold;
margin-right: 10px;
color: $grey-foreground-color;
- min-width: 20px;
+ min-width: 25px;
my-global-icon {
@include apply-svg-color($grey-foreground-color);
a {
color: var(--mainForegroundColor);
- width: fit-content;
+ width: auto;
&:hover {
text-decoration: underline !important;
-import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core'
import { Video } from '@app/shared/video/video.model'
import { VideoPlaylistElementUpdate } from '@shared/models'
import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
@Component({
selector: 'my-video-playlist-element-miniature',
styleUrls: [ './video-playlist-element-miniature.component.scss' ],
- templateUrl: './video-playlist-element-miniature.component.html'
+ templateUrl: './video-playlist-element-miniature.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class VideoPlaylistElementMiniatureComponent {
@ViewChild('moreDropdown') moreDropdown: NgbDropdown
@Input() playing = false
@Input() rowLink = false
@Input() accountLink = true
+ @Input() position: number
@Output() elementRemoved = new EventEmitter<Video>()
private route: ActivatedRoute,
private i18n: I18n,
private videoService: VideoService,
- private videoPlaylistService: VideoPlaylistService
+ private videoPlaylistService: VideoPlaylistService,
+ private cdr: ChangeDetectorRef
) {}
buildRouterLink () {
video.playlistElement.startTimestamp = body.startTimestamp
video.playlistElement.stopTimestamp = body.stopTimestamp
+
+ this.cdr.detectChanges()
},
err => this.notifier.error(err.message)
this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp
}
}
+
+ // FIXME: why do we have to use setTimeout here?
+ setTimeout(() => {
+ this.cdr.detectChanges()
+ })
}
}
<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage }">
<a
- [routerLink]="getPlaylistUrl()" [attr.title]="playlist.displayName"
+ [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description"
class="miniature-thumbnail"
>
<img alt="" [attr.aria-labelledby]="playlist.displayName" [attr.src]="playlist.thumbnailUrl" />
</div>
</a>
- <div class="miniature-bottom">
- <a tabindex="-1" class="miniature-name" [routerLink]="getPlaylistUrl()" [attr.title]="playlist.displayName">
+ <div class="miniature-info">
+ <a tabindex="-1" class="miniature-name" [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description">
{{ playlist.displayName }}
</a>
+
+ <div class="video-info-privacy" *ngIf="displayPrivacy">{{ playlist.privacy.label }}</div>
+
+ <div class="video-info-by-date">
+ <a i18n [routerLink]="[ '/video-channels', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy">
+ {{ playlist.videoChannelBy }}
+ </a>
+
+ <div i18n class="updated-at">Updated {{ playlist.updatedAt | myFromNow }}</div>
+ </div>
+
+ <div *ngIf="displayDescription" class="video-info-description">{{ playlist.description }}</div>
</div>
</div>
}
}
- &.to-manage .play-overlay,
+ &.to-manage,
&.no-videos {
- display: none;
+ .play-overlay {
+ display: none;
+ }
}
.miniature-thumbnail {
}
}
- .miniature-bottom {
+ .miniature-info {
width: 200px;
margin-top: 2px;
line-height: normal;
.miniature-name {
@include miniature-name;
}
+
+ .video-info-by-date {
+ display: flex;
+ font-size: 13px;
+ margin: 5px 0;
+
+ .by {
+ @include disable-default-a-behaviour;
+
+ display: block;
+ color: var(--mainForegroundColor);
+
+ &::after {
+ content: '-';
+ margin: 0 3px;
+ }
+ }
+ }
+
+ .video-info-privacy {
+ font-size: 13px;
+ font-weight: $font-semibold;
+ }
+
+ .video-info-description {
+ margin-top: 10px;
+ color: $grey-foreground-color;
+ }
}
}
export class VideoPlaylistMiniatureComponent {
@Input() playlist: VideoPlaylist
@Input() toManage = false
+ @Input() displayChannel = false
+ @Input() displayDescription = false
+ @Input() displayPrivacy = false
getPlaylistUrl () {
if (this.toManage) return [ '/my-account/video-playlists', this.playlist.uuid ]
import { distinct, distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators'
-import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'
+import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'
import { fromEvent, Subscription } from 'rxjs'
@Directive({
@Input() firstLoadedPage = 1
@Input() percentLimit = 70
@Input() autoInit = false
- @Input() container = document.body
+ @Input() onItself = false
@Output() nearOfBottom = new EventEmitter<void>()
@Output() nearOfTop = new EventEmitter<void>()
private scrollUpSub: Subscription
private pageChangeSub: Subscription
private middleScreen: number
+ private container: HTMLElement
- constructor () {
+ constructor (private el: ElementRef) {
this.decimalLimit = this.percentLimit / 100
}
}
initialize () {
+ if (this.onItself) {
+ this.container = this.el.nativeElement
+ }
+
this.middleScreen = window.innerHeight / 2
// Emit the last value
const throttleOptions = { leading: true, trailing: true }
- const scrollObservable = fromEvent(window, 'scroll')
+ const scrollObservable = fromEvent(this.container || window, 'scroll')
.pipe(
startWith(null),
throttleTime(200, undefined, throttleOptions),
- map(() => ({ current: window.scrollY, maximumScroll: this.container.clientHeight - window.innerHeight })),
+ map(() => this.getScrollInfo()),
distinctUntilChanged((o1, o2) => o1.current === o2.current),
share()
)
// Offset page
return page + (this.firstLoadedPage - 1)
}
+
+ private getScrollInfo () {
+ if (this.container) {
+ return { current: this.container.scrollTop, maximumScroll: this.container.scrollHeight }
+ }
+
+ return { current: window.scrollY, maximumScroll: document.body.clientHeight - window.innerHeight }
+ }
}
<div id="videojs-wrapper"></div>
- <div *ngIf="playlist && video" class="playlist">
+ <div *ngIf="playlist && video" class="playlist" myInfiniteScroller [autoInit]="true" [onItself]="true" (nearOfBottom)="onPlaylistVideosNearOfBottom()">
<div class="playlist-info">
<div class="playlist-display-name">
{{ playlist.displayName }}
</div>
</div>
- <div *ngFor="let playlistVideo of playlistVideos" myInfiniteScroller [autoInit]="true" #elem [container]="elem" (nearOfBottom)="onPlaylistVideosNearOfBottom()">
+ <div *ngFor="let playlistVideo of playlistVideos">
<my-video-playlist-element-miniature
[video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
- [playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false"
+ [playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false" [position]="playlistVideo.playlistElement.position"
></my-video-playlist-element-miniature>
</div>
</div>
.playlist {
width: 400px;
height: 66vh;
- background-color: #e4e4e4;
+ background-color: var(--mainBackgroundColor);
overflow-y: auto;
.playlist-info {
padding: 5px 30px;
+ background-color: #e4e4e4;
.playlist-display-name {
font-size: 18px;
playlistVideos: Video[] = []
playlistPagination: ComponentPagination = {
currentPage: 1,
- itemsPerPage: 10,
+ itemsPerPage: 30,
totalItems: null
}
noPlaylistVideos = false
}
private loadPlaylistElements (redirectToFirst = false) {
- this.videoService.getPlaylistVideos(this.playlist.id, this.playlistPagination)
+ this.videoService.getPlaylistVideos(this.playlist.uuid, this.playlistPagination)
.subscribe(({ totalVideos, videos }) => {
this.playlistVideos = this.playlistVideos.concat(videos)
this.playlistPagination.totalItems = totalVideos
background-color: var(--submenuColor);
width: 100%;
height: 81px;
- margin-bottom: 30px;
+ margin-bottom: $sub-menu-margin-bottom;
display: flex;
align-items: center;
padding-left: $not-expanded-horizontal-margins;
$input-background-color: $bg-color;
$input-placeholder-color: #898989;
+$sub-menu-margin-bottom: 30px;
+
/*** map theme ***/
// pass variables into a sass map,