Add playlist search option and search input for add-to-video-playlist dropdown
fixes #2138
This commit is contained in:
parent
def2a70b7e
commit
c06af5012e
8 changed files with 85 additions and 11 deletions
|
@ -34,6 +34,7 @@
|
||||||
|
|
||||||
& > my-user-notifications:nth-child(2) {
|
& > my-user-notifications:nth-child(2) {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" placeholder="Search playlists" [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="playlists">
|
<div class="playlists">
|
||||||
<div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
|
<div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
|
||||||
<my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox>
|
<my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox>
|
||||||
|
|
|
@ -53,6 +53,15 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0 15px 10px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.playlist {
|
.playlist {
|
||||||
display: flex;
|
display: flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -76,7 +85,6 @@
|
||||||
.new-playlist-button,
|
.new-playlist-button,
|
||||||
.new-playlist-block {
|
.new-playlist-block {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
margin-top: 10px;
|
|
||||||
border-top: 1px solid $separator-border-color;
|
border-top: 1px solid $separator-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
||||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||||
import { AuthService, Notifier } from '@app/core'
|
import { AuthService, Notifier } from '@app/core'
|
||||||
import { forkJoin } from 'rxjs'
|
import { forkJoin, Subject } from 'rxjs'
|
||||||
|
import { debounceTime } from 'rxjs/operators'
|
||||||
import { Video, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models'
|
import { Video, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models'
|
||||||
import { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/forms'
|
import { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/forms'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
@ -29,6 +30,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||||
@Input() lazyLoad = false
|
@Input() lazyLoad = false
|
||||||
|
|
||||||
isNewPlaylistBlockOpened = false
|
isNewPlaylistBlockOpened = false
|
||||||
|
videoPlaylistSearch: string
|
||||||
|
videoPlaylistSearchChanged = new Subject<string>()
|
||||||
videoPlaylists: PlaylistSummary[] = []
|
videoPlaylists: PlaylistSummary[] = []
|
||||||
timestampOptions: {
|
timestampOptions: {
|
||||||
startTimestampEnabled: boolean
|
startTimestampEnabled: boolean
|
||||||
|
@ -58,6 +61,13 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||||
this.buildForm({
|
this.buildForm({
|
||||||
displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME
|
displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.videoPlaylistSearchChanged
|
||||||
|
.pipe(
|
||||||
|
debounceTime(500))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.load()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges (simpleChanges: SimpleChanges) {
|
ngOnChanges (simpleChanges: SimpleChanges) {
|
||||||
|
@ -74,6 +84,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||||
|
|
||||||
reload () {
|
reload () {
|
||||||
this.videoPlaylists = []
|
this.videoPlaylists = []
|
||||||
|
this.videoPlaylistSearch = undefined
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
|
|
||||||
|
@ -82,11 +93,12 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||||
|
|
||||||
load () {
|
load () {
|
||||||
forkJoin([
|
forkJoin([
|
||||||
this.videoPlaylistService.listAccountPlaylists(this.user.account, undefined,'-updatedAt'),
|
this.videoPlaylistService.listAccountPlaylists(this.user.account, undefined, '-updatedAt', this.videoPlaylistSearch),
|
||||||
this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id)
|
this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id)
|
||||||
])
|
])
|
||||||
.subscribe(
|
.subscribe(
|
||||||
([ playlistsResult, existResult ]) => {
|
([ playlistsResult, existResult ]) => {
|
||||||
|
this.videoPlaylists = []
|
||||||
for (const playlist of playlistsResult.data) {
|
for (const playlist of playlistsResult.data) {
|
||||||
const existingPlaylist = existResult[ this.video.id ].find(p => p.playlistId === playlist.id)
|
const existingPlaylist = existResult[ this.video.id ].find(p => p.playlistId === playlist.id)
|
||||||
|
|
||||||
|
@ -178,6 +190,10 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||||
return `(${start}-${stop})`
|
return `(${start}-${stop})`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onVideoPlaylistSearchChanged () {
|
||||||
|
this.videoPlaylistSearchChanged.next()
|
||||||
|
}
|
||||||
|
|
||||||
private removeVideoFromPlaylist (playlist: PlaylistSummary) {
|
private removeVideoFromPlaylist (playlist: PlaylistSummary) {
|
||||||
if (!playlist.playlistElementId) return
|
if (!playlist.playlistElementId) return
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,12 @@ export class VideoPlaylistService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
listAccountPlaylists (account: Account, componentPagination: ComponentPagination, sort: string): Observable<ResultList<VideoPlaylist>> {
|
listAccountPlaylists (
|
||||||
|
account: Account,
|
||||||
|
componentPagination: ComponentPagination,
|
||||||
|
sort: string,
|
||||||
|
search?: string
|
||||||
|
): Observable<ResultList<VideoPlaylist>> {
|
||||||
const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists'
|
const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists'
|
||||||
const pagination = componentPagination
|
const pagination = componentPagination
|
||||||
? this.restService.componentPaginationToRestPagination(componentPagination)
|
? this.restService.componentPaginationToRestPagination(componentPagination)
|
||||||
|
@ -67,6 +72,7 @@ export class VideoPlaylistService {
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
if (search) params = this.restService.addObjectParams(params, { search })
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params })
|
return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -213,8 +219,8 @@ export class VideoPlaylistService {
|
||||||
|
|
||||||
private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
|
private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
|
||||||
const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
|
const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
|
||||||
let params = new HttpParams()
|
|
||||||
|
|
||||||
|
let params = new HttpParams()
|
||||||
params = this.restService.addObjectParams(params, { videoIds })
|
params = this.restService.addObjectParams(params, { videoIds })
|
||||||
|
|
||||||
return this.authHttp.get<VideoExistInPlaylist>(url, { params })
|
return this.authHttp.get<VideoExistInPlaylist>(url, { params })
|
||||||
|
|
|
@ -27,7 +27,10 @@ import { VideoChannelModel } from '../../models/video/video-channel'
|
||||||
import { JobQueue } from '../../lib/job-queue'
|
import { JobQueue } from '../../lib/job-queue'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { VideoPlaylistModel } from '../../models/video/video-playlist'
|
import { VideoPlaylistModel } from '../../models/video/video-playlist'
|
||||||
import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
|
import {
|
||||||
|
commonVideoPlaylistFiltersValidator,
|
||||||
|
videoPlaylistsSearchValidator
|
||||||
|
} from '../../middlewares/validators/videos/video-playlists'
|
||||||
|
|
||||||
const accountsRouter = express.Router()
|
const accountsRouter = express.Router()
|
||||||
|
|
||||||
|
@ -72,6 +75,7 @@ accountsRouter.get('/:accountName/video-playlists',
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
commonVideoPlaylistFiltersValidator,
|
commonVideoPlaylistFiltersValidator,
|
||||||
|
videoPlaylistsSearchValidator,
|
||||||
asyncMiddleware(listAccountPlaylists)
|
asyncMiddleware(listAccountPlaylists)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -135,6 +139,7 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultList = await VideoPlaylistModel.listForApi({
|
const resultList = await VideoPlaylistModel.listForApi({
|
||||||
|
search: req.query.search,
|
||||||
followerActorId: serverActor.id,
|
followerActorId: serverActor.id,
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
|
|
|
@ -166,6 +166,18 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const videoPlaylistsSearchValidator = [
|
||||||
|
query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking videoPlaylists search query', { parameters: req.query })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const videoPlaylistsAddVideoValidator = [
|
const videoPlaylistsAddVideoValidator = [
|
||||||
param('playlistId')
|
param('playlistId')
|
||||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
|
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
|
||||||
|
@ -354,6 +366,7 @@ export {
|
||||||
videoPlaylistsUpdateValidator,
|
videoPlaylistsUpdateValidator,
|
||||||
videoPlaylistsDeleteValidator,
|
videoPlaylistsDeleteValidator,
|
||||||
videoPlaylistsGetValidator,
|
videoPlaylistsGetValidator,
|
||||||
|
videoPlaylistsSearchValidator,
|
||||||
|
|
||||||
videoPlaylistsAddVideoValidator,
|
videoPlaylistsAddVideoValidator,
|
||||||
videoPlaylistsUpdateOrRemoveVideoValidator,
|
videoPlaylistsUpdateOrRemoveVideoValidator,
|
||||||
|
|
|
@ -13,10 +13,11 @@ import {
|
||||||
Model,
|
Model,
|
||||||
Scopes,
|
Scopes,
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt,
|
||||||
|
Sequelize
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
||||||
import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils'
|
import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid, createSimilarityAttribute } from '../utils'
|
||||||
import {
|
import {
|
||||||
isVideoPlaylistDescriptionValid,
|
isVideoPlaylistDescriptionValid,
|
||||||
isVideoPlaylistNameValid,
|
isVideoPlaylistNameValid,
|
||||||
|
@ -67,7 +68,8 @@ type AvailableForListOptions = {
|
||||||
type?: VideoPlaylistType
|
type?: VideoPlaylistType
|
||||||
accountId?: number
|
accountId?: number
|
||||||
videoChannelId?: number
|
videoChannelId?: number
|
||||||
privateAndUnlisted?: boolean
|
privateAndUnlisted?: boolean,
|
||||||
|
search?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scopes(() => ({
|
@Scopes(() => ({
|
||||||
|
@ -163,6 +165,23 @@ type AvailableForListOptions = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.search) {
|
||||||
|
const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search)
|
||||||
|
const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%')
|
||||||
|
whereAnd.push({
|
||||||
|
id: {
|
||||||
|
[ Op.in ]: Sequelize.literal(
|
||||||
|
'(' +
|
||||||
|
'SELECT "videoPlaylist"."id" FROM "videoPlaylist" ' +
|
||||||
|
'WHERE ' +
|
||||||
|
'lower(immutable_unaccent("videoPlaylist"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
|
||||||
|
'lower(immutable_unaccent("videoPlaylist"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
|
||||||
|
')'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const where = {
|
const where = {
|
||||||
[Op.and]: whereAnd
|
[Op.and]: whereAnd
|
||||||
}
|
}
|
||||||
|
@ -291,7 +310,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
type?: VideoPlaylistType,
|
type?: VideoPlaylistType,
|
||||||
accountId?: number,
|
accountId?: number,
|
||||||
videoChannelId?: number,
|
videoChannelId?: number,
|
||||||
privateAndUnlisted?: boolean
|
privateAndUnlisted?: boolean,
|
||||||
|
search?: string
|
||||||
}) {
|
}) {
|
||||||
const query = {
|
const query = {
|
||||||
offset: options.start,
|
offset: options.start,
|
||||||
|
@ -308,7 +328,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
followerActorId: options.followerActorId,
|
followerActorId: options.followerActorId,
|
||||||
accountId: options.accountId,
|
accountId: options.accountId,
|
||||||
videoChannelId: options.videoChannelId,
|
videoChannelId: options.videoChannelId,
|
||||||
privateAndUnlisted: options.privateAndUnlisted
|
privateAndUnlisted: options.privateAndUnlisted,
|
||||||
|
search: options.search
|
||||||
} as AvailableForListOptions
|
} as AvailableForListOptions
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue