Add search bars for a user's videos and playlist library
This commit is contained in:
parent
71810d0bcb
commit
bf64ed4196
|
@ -1,4 +1,6 @@
|
||||||
<div class="video-playlists-header">
|
<div class="video-playlists-header">
|
||||||
|
<input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
|
||||||
|
|
||||||
<a class="create-button" routerLink="create">
|
<a class="create-button" routerLink="create">
|
||||||
<my-global-icon iconName="add"></my-global-icon>
|
<my-global-icon iconName="add"></my-global-icon>
|
||||||
<ng-container i18n>Create a new playlist</ng-container>
|
<ng-container i18n>Create a new playlist</ng-container>
|
||||||
|
|
|
@ -29,12 +29,19 @@
|
||||||
|
|
||||||
.video-playlist-buttons {
|
.video-playlist-buttons {
|
||||||
min-width: 190px;
|
min-width: 190px;
|
||||||
|
height: max-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-playlists-header {
|
.video-playlists-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: 20px 0 50px;
|
margin: 20px 0 50px;
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
@include peertube-input-text(300px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Notifier } from '@app/core'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { ConfirmService } from '../../core/confirm'
|
import { ConfirmService } from '../../core/confirm'
|
||||||
import { User } from '@app/shared'
|
import { User } from '@app/shared'
|
||||||
import { flatMap } from 'rxjs/operators'
|
import { flatMap, debounceTime } from 'rxjs/operators'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||||
|
@ -17,7 +17,9 @@ import { Subject } from 'rxjs'
|
||||||
styleUrls: [ './my-account-video-playlists.component.scss' ]
|
styleUrls: [ './my-account-video-playlists.component.scss' ]
|
||||||
})
|
})
|
||||||
export class MyAccountVideoPlaylistsComponent implements OnInit {
|
export class MyAccountVideoPlaylistsComponent implements OnInit {
|
||||||
|
videoPlaylistsSearch: string
|
||||||
videoPlaylists: VideoPlaylist[] = []
|
videoPlaylists: VideoPlaylist[] = []
|
||||||
|
videoPlaylistSearchChanged = new Subject<string>()
|
||||||
|
|
||||||
pagination: ComponentPagination = {
|
pagination: ComponentPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
@ -41,6 +43,13 @@ export class MyAccountVideoPlaylistsComponent implements OnInit {
|
||||||
this.user = this.authService.getUser()
|
this.user = this.authService.getUser()
|
||||||
|
|
||||||
this.loadVideoPlaylists()
|
this.loadVideoPlaylists()
|
||||||
|
|
||||||
|
this.videoPlaylistSearchChanged
|
||||||
|
.pipe(
|
||||||
|
debounceTime(500))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.loadVideoPlaylists()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) {
|
async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) {
|
||||||
|
@ -80,12 +89,17 @@ export class MyAccountVideoPlaylistsComponent implements OnInit {
|
||||||
this.loadVideoPlaylists()
|
this.loadVideoPlaylists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onVideoPlaylistSearchChanged () {
|
||||||
|
this.videoPlaylistSearchChanged.next()
|
||||||
|
}
|
||||||
|
|
||||||
private loadVideoPlaylists () {
|
private loadVideoPlaylists () {
|
||||||
this.authService.userInformationLoaded
|
this.authService.userInformationLoaded
|
||||||
.pipe(flatMap(() => {
|
.pipe(flatMap(() => {
|
||||||
return this.videoPlaylistService.listAccountPlaylists(this.user.account, this.pagination, '-updatedAt')
|
return this.videoPlaylistService.listAccountPlaylists(this.user.account, this.pagination, '-updatedAt', this.videoPlaylistsSearch)
|
||||||
}))
|
}))
|
||||||
.subscribe(res => {
|
.subscribe(res => {
|
||||||
|
this.videoPlaylists = []
|
||||||
this.videoPlaylists = this.videoPlaylists.concat(res.data)
|
this.videoPlaylists = this.videoPlaylists.concat(res.data)
|
||||||
this.pagination.totalItems = res.total
|
this.pagination.totalItems = res.total
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
<div class="videos-header">
|
||||||
|
<input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch" (ngModelChange)="onVideosSearchChanged()" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<my-videos-selection
|
<my-videos-selection
|
||||||
[pagination]="pagination"
|
[pagination]="pagination"
|
||||||
[(selection)]="selection"
|
[(selection)]="selection"
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
@import '_variables';
|
@import '_variables';
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
|
.videos-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
text-align: right;
|
||||||
|
margin: 20px 0 50px;
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
@include peertube-input-text(300px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.action-button-delete-selection {
|
.action-button-delete-selection {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { concat, Observable } from 'rxjs'
|
import { concat, Observable, Subject } from 'rxjs'
|
||||||
import { tap, toArray } from 'rxjs/operators'
|
import { tap, toArray, debounceTime } from 'rxjs/operators'
|
||||||
import { Component, ViewChild } from '@angular/core'
|
import { Component, ViewChild, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||||
|
@ -22,7 +22,7 @@ import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
|
||||||
templateUrl: './my-account-videos.component.html',
|
templateUrl: './my-account-videos.component.html',
|
||||||
styleUrls: [ './my-account-videos.component.scss' ]
|
styleUrls: [ './my-account-videos.component.scss' ]
|
||||||
})
|
})
|
||||||
export class MyAccountVideosComponent implements DisableForReuseHook {
|
export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
|
||||||
@ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent
|
@ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent
|
||||||
@ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent
|
@ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent
|
||||||
|
|
||||||
|
@ -43,6 +43,8 @@ export class MyAccountVideosComponent implements DisableForReuseHook {
|
||||||
blacklistInfo: true
|
blacklistInfo: true
|
||||||
}
|
}
|
||||||
videos: Video[] = []
|
videos: Video[] = []
|
||||||
|
videosSearch: string
|
||||||
|
videosSearchChanged = new Subject<string>()
|
||||||
getVideosObservableFunction = this.getVideosObservable.bind(this)
|
getVideosObservableFunction = this.getVideosObservable.bind(this)
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -59,6 +61,19 @@ export class MyAccountVideosComponent implements DisableForReuseHook {
|
||||||
this.titlePage = this.i18n('My videos')
|
this.titlePage = this.i18n('My videos')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.videosSearchChanged
|
||||||
|
.pipe(
|
||||||
|
debounceTime(500))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.videosSelection.reloadVideos()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onVideosSearchChanged () {
|
||||||
|
this.videosSearchChanged.next()
|
||||||
|
}
|
||||||
|
|
||||||
disableForReuse () {
|
disableForReuse () {
|
||||||
this.videosSelection.disableForReuse()
|
this.videosSelection.disableForReuse()
|
||||||
}
|
}
|
||||||
|
@ -70,7 +85,7 @@ export class MyAccountVideosComponent implements DisableForReuseHook {
|
||||||
getVideosObservable (page: number, sort: VideoSortField) {
|
getVideosObservable (page: number, sort: VideoSortField) {
|
||||||
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||||
|
|
||||||
return this.videoService.getMyVideos(newPagination, sort)
|
return this.videoService.getMyVideos(newPagination, sort, this.videosSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSelectedVideos () {
|
async deleteSelectedVideos () {
|
||||||
|
|
|
@ -88,7 +88,7 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
getMyVideoQuotaUsed () {
|
getMyVideoQuotaUsed () {
|
||||||
const url = UserService.BASE_USERS_URL + '/me/video-quota-used'
|
const url = UserService.BASE_USERS_URL + 'me/video-quota-used'
|
||||||
|
|
||||||
return this.authHttp.get<UserVideoQuota>(url)
|
return this.authHttp.get<UserVideoQuota>(url)
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" placeholder="Search playlists" [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
|
<input type="text" placeholder="Search playlists" i18n-placeholder [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="playlists">
|
<div class="playlists">
|
||||||
|
|
|
@ -150,6 +150,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
|
||||||
this.getVideosObservable(this.pagination.currentPage).subscribe(
|
this.getVideosObservable(this.pagination.currentPage).subscribe(
|
||||||
({ data, total }) => {
|
({ data, total }) => {
|
||||||
this.pagination.totalItems = total
|
this.pagination.totalItems = total
|
||||||
|
this.videos = []
|
||||||
this.videos = this.videos.concat(data)
|
this.videos = this.videos.concat(data)
|
||||||
|
|
||||||
if (this.groupByDate) this.buildGroupedDateLabels()
|
if (this.groupByDate) this.buildGroupedDateLabels()
|
||||||
|
@ -170,7 +171,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
|
||||||
|
|
||||||
reloadVideos () {
|
reloadVideos () {
|
||||||
this.pagination.currentPage = 1
|
this.pagination.currentPage = 1
|
||||||
this.videos = []
|
|
||||||
this.loadMoreVideos()
|
this.loadMoreVideos()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,8 @@ $more-margin-right: 15px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
my-video-thumbnail {
|
my-video-thumbnail {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|
|
@ -121,14 +121,15 @@ export class VideoService implements VideosProvider {
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<ResultList<Video>> {
|
getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField, search?: string): Observable<ResultList<Video>> {
|
||||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
params = this.restService.addObjectParams(params, { search })
|
||||||
|
|
||||||
return this.authHttp
|
return this.authHttp
|
||||||
.get<ResultList<Video>>(UserService.BASE_USERS_URL + '/me/videos', { params })
|
.get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(res => this.extractVideos(res)),
|
switchMap(res => this.extractVideos(res)),
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
border: none;
|
border: none;
|
||||||
|
transform: translateY(-7%);
|
||||||
}
|
}
|
||||||
|
|
||||||
my-feed {
|
my-feed {
|
||||||
|
|
|
@ -10,6 +10,12 @@
|
||||||
color: var(--mainForegroundColor) !important;
|
color: var(--mainForegroundColor) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my-edit-button,
|
||||||
|
my-delete-button,
|
||||||
|
my-button {
|
||||||
|
height: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
// data table customizations
|
// data table customizations
|
||||||
p-table {
|
p-table {
|
||||||
.ui-table-caption {
|
.ui-table-caption {
|
||||||
|
|
|
@ -99,7 +99,8 @@ async function getUserVideos (req: express.Request, res: express.Response) {
|
||||||
user.Account.id,
|
user.Account.id,
|
||||||
req.query.start as number,
|
req.query.start as number,
|
||||||
req.query.count as number,
|
req.query.count as number,
|
||||||
req.query.sort as VideoSortField
|
req.query.sort as VideoSortField,
|
||||||
|
req.query.search as string
|
||||||
)
|
)
|
||||||
|
|
||||||
const additionalAttributes = {
|
const additionalAttributes = {
|
||||||
|
|
|
@ -1196,9 +1196,15 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static listUserVideosForApi (accountId: number, start: number, count: number, sort: string) {
|
static listUserVideosForApi (
|
||||||
|
accountId: number,
|
||||||
|
start: number,
|
||||||
|
count: number,
|
||||||
|
sort: string,
|
||||||
|
search?: string
|
||||||
|
) {
|
||||||
function buildBaseQuery (): FindOptions {
|
function buildBaseQuery (): FindOptions {
|
||||||
return {
|
let baseQuery = {
|
||||||
offset: start,
|
offset: start,
|
||||||
limit: count,
|
limit: count,
|
||||||
order: getVideoSort(sort),
|
order: getVideoSort(sort),
|
||||||
|
@ -1218,12 +1224,24 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
baseQuery = Object.assign(baseQuery, {
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
[ Op.iLike ]: '%' + search + '%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
const countQuery = buildBaseQuery()
|
const countQuery = buildBaseQuery()
|
||||||
const findQuery = buildBaseQuery()
|
const findQuery = buildBaseQuery()
|
||||||
|
|
||||||
const findScopes = [
|
const findScopes: (string | ScopeOptions)[] = [
|
||||||
ScopeNames.WITH_SCHEDULED_UPDATE,
|
ScopeNames.WITH_SCHEDULED_UPDATE,
|
||||||
ScopeNames.WITH_BLACKLISTED,
|
ScopeNames.WITH_BLACKLISTED,
|
||||||
ScopeNames.WITH_THUMBNAILS
|
ScopeNames.WITH_THUMBNAILS
|
||||||
|
|
Loading…
Reference in New Issue