Add video channel view

pull/1745/head
Chocobozzz 2019-03-14 14:05:36 +01:00 committed by Chocobozzz
parent 2a10aab3d7
commit bce47964f6
26 changed files with 251 additions and 48 deletions

View File

@ -84,15 +84,6 @@ const myAccountRoutes: Routes = [
}
}
},
{
path: 'video-playlists/:videoPlaylistId',
component: MyAccountVideoPlaylistElementsComponent,
data: {
meta: {
title: 'Playlist elements'
}
}
},
{
path: 'video-playlists/create',
component: MyAccountVideoPlaylistCreateComponent,
@ -102,6 +93,15 @@ const myAccountRoutes: Routes = [
}
}
},
{
path: 'video-playlists/:videoPlaylistId',
component: MyAccountVideoPlaylistElementsComponent,
data: {
meta: {
title: 'Playlist elements'
}
}
},
{
path: 'video-playlists/update/:videoPlaylistId',
component: MyAccountVideoPlaylistUpdateComponent,

View File

@ -1,11 +1,26 @@
<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>

View File

@ -2,6 +2,17 @@
@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;

View File

@ -24,7 +24,7 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
pagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 10,
itemsPerPage: 30,
totalItems: null
}
@ -123,6 +123,10 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
this.loadElements()
}
trackByFn (index: number, elem: Video) {
return elem.id
}
private loadElements () {
this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
.subscribe(({ totalVideos, videos }) => {

View File

@ -8,7 +8,8 @@
<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">

View File

@ -20,8 +20,9 @@
/deep/ .miniature {
display: flex;
.miniature-bottom {
.miniature-info {
margin-left: 10px;
width: auto;
}
}
}

View File

@ -17,7 +17,7 @@
<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>

View File

@ -64,11 +64,11 @@
}
.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;
}

View File

@ -0,0 +1,11 @@
<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>

View File

@ -0,0 +1,9 @@
.video-playlist {
display: flex;
justify-content: center;
my-video-playlist-miniature {
margin-right: 15px;
margin-bottom: 30px;
}
}

View File

@ -0,0 +1,67 @@
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
})
}
}

View File

@ -4,6 +4,7 @@ import { MetaGuard } from '@ngx-meta/core'
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 = [
{
@ -25,6 +26,15 @@ const videoChannelsRoutes: Routes = [
}
}
},
{
path: 'video-playlists',
component: VideoChannelPlaylistsComponent,
data: {
meta: {
title: 'Video channel playlists'
}
}
},
{
path: 'about',
component: VideoChannelAboutComponent,

View File

@ -22,6 +22,7 @@
<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>

View File

@ -4,6 +4,7 @@ import { VideoChannelsRoutingModule } from './video-channels-routing.module'
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: [
@ -14,7 +15,8 @@ import { VideoChannelAboutComponent } from './video-channel-about/video-channel-
declarations: [
VideoChannelsComponent,
VideoChannelVideosComponent,
VideoChannelAboutComponent
VideoChannelAboutComponent,
VideoChannelPlaylistsComponent
],
exports: [

View File

@ -2,7 +2,7 @@
<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

View File

@ -34,7 +34,7 @@
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);
@ -59,7 +59,7 @@
a {
color: var(--mainForegroundColor);
width: fit-content;
width: auto;
&:hover {
text-decoration: underline !important;

View File

@ -1,4 +1,4 @@
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'
@ -13,7 +13,8 @@ import { secondsToTime } from '../../../assets/player/utils'
@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
@ -24,6 +25,7 @@ export class VideoPlaylistElementMiniatureComponent {
@Input() playing = false
@Input() rowLink = false
@Input() accountLink = true
@Input() position: number
@Output() elementRemoved = new EventEmitter<Video>()
@ -44,7 +46,8 @@ export class VideoPlaylistElementMiniatureComponent {
private route: ActivatedRoute,
private i18n: I18n,
private videoService: VideoService,
private videoPlaylistService: VideoPlaylistService
private videoPlaylistService: VideoPlaylistService,
private cdr: ChangeDetectorRef
) {}
buildRouterLink () {
@ -95,6 +98,8 @@ export class VideoPlaylistElementMiniatureComponent {
video.playlistElement.startTimestamp = body.startTimestamp
video.playlistElement.stopTimestamp = body.stopTimestamp
this.cdr.detectChanges()
},
err => this.notifier.error(err.message)
@ -145,5 +150,10 @@ export class VideoPlaylistElementMiniatureComponent {
this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp
}
}
// FIXME: why do we have to use setTimeout here?
setTimeout(() => {
this.cdr.detectChanges()
})
}
}

View File

@ -1,6 +1,6 @@
<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" />
@ -14,9 +14,21 @@
</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>

View File

@ -11,9 +11,11 @@
}
}
&.to-manage .play-overlay,
&.to-manage,
&.no-videos {
display: none;
.play-overlay {
display: none;
}
}
.miniature-thumbnail {
@ -34,7 +36,7 @@
}
}
.miniature-bottom {
.miniature-info {
width: 200px;
margin-top: 2px;
line-height: normal;
@ -42,5 +44,33 @@
.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;
}
}
}

View File

@ -9,6 +9,9 @@ import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
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 ]

View File

@ -1,5 +1,5 @@
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({
@ -11,7 +11,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
@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>()
@ -24,8 +24,9 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
private scrollUpSub: Subscription
private pageChangeSub: Subscription
private middleScreen: number
private container: HTMLElement
constructor () {
constructor (private el: ElementRef) {
this.decimalLimit = this.percentLimit / 100
}
@ -40,16 +41,20 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
}
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()
)
@ -102,4 +107,12 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
// 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 }
}
}

View File

@ -9,7 +9,7 @@
<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 }}
@ -27,10 +27,10 @@
</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>

View File

@ -43,11 +43,12 @@ $other-videos-width: 260px;
.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;

View File

@ -58,7 +58,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
playlistVideos: Video[] = []
playlistPagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 10,
itemsPerPage: 30,
totalItems: null
}
noPlaylistVideos = false
@ -401,7 +401,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
}
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

View File

@ -104,7 +104,7 @@ label {
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;

View File

@ -54,6 +54,8 @@ $theater-bottom-space: 115px;
$input-background-color: $bg-color;
$input-placeholder-color: #898989;
$sub-menu-margin-bottom: 30px;
/*** map theme ***/
// pass variables into a sass map,