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) {
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-container">
|
||||
<input type="text" placeholder="Search playlists" [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
|
||||
</div>
|
||||
|
||||
<div class="playlists">
|
||||
<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>
|
||||
|
|
|
@ -53,6 +53,15 @@
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
flex-grow: 1;
|
||||
margin: 0 15px 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
|
@ -76,7 +85,6 @@
|
|||
.new-playlist-button,
|
||||
.new-playlist-block {
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid $separator-border-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||
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 { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/forms'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
|
@ -29,6 +30,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
|||
@Input() lazyLoad = false
|
||||
|
||||
isNewPlaylistBlockOpened = false
|
||||
videoPlaylistSearch: string
|
||||
videoPlaylistSearchChanged = new Subject<string>()
|
||||
videoPlaylists: PlaylistSummary[] = []
|
||||
timestampOptions: {
|
||||
startTimestampEnabled: boolean
|
||||
|
@ -58,6 +61,13 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
|||
this.buildForm({
|
||||
displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME
|
||||
})
|
||||
|
||||
this.videoPlaylistSearchChanged
|
||||
.pipe(
|
||||
debounceTime(500))
|
||||
.subscribe(() => {
|
||||
this.load()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnChanges (simpleChanges: SimpleChanges) {
|
||||
|
@ -74,6 +84,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
|||
|
||||
reload () {
|
||||
this.videoPlaylists = []
|
||||
this.videoPlaylistSearch = undefined
|
||||
|
||||
this.init()
|
||||
|
||||
|
@ -82,11 +93,12 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
|||
|
||||
load () {
|
||||
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)
|
||||
])
|
||||
.subscribe(
|
||||
([ playlistsResult, existResult ]) => {
|
||||
this.videoPlaylists = []
|
||||
for (const playlist of playlistsResult.data) {
|
||||
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})`
|
||||
}
|
||||
|
||||
onVideoPlaylistSearchChanged () {
|
||||
this.videoPlaylistSearchChanged.next()
|
||||
}
|
||||
|
||||
private removeVideoFromPlaylist (playlist: PlaylistSummary) {
|
||||
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 pagination = componentPagination
|
||||
? this.restService.componentPaginationToRestPagination(componentPagination)
|
||||
|
@ -67,6 +72,7 @@ export class VideoPlaylistService {
|
|||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
if (search) params = this.restService.addObjectParams(params, { search })
|
||||
|
||||
return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params })
|
||||
.pipe(
|
||||
|
@ -213,8 +219,8 @@ export class VideoPlaylistService {
|
|||
|
||||
private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
|
||||
const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
|
||||
let params = new HttpParams()
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addObjectParams(params, { videoIds })
|
||||
|
||||
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 { logger } from '../../helpers/logger'
|
||||
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()
|
||||
|
||||
|
@ -72,6 +75,7 @@ accountsRouter.get('/:accountName/video-playlists',
|
|||
setDefaultSort,
|
||||
setDefaultPagination,
|
||||
commonVideoPlaylistFiltersValidator,
|
||||
videoPlaylistsSearchValidator,
|
||||
asyncMiddleware(listAccountPlaylists)
|
||||
)
|
||||
|
||||
|
@ -135,6 +139,7 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
|
|||
}
|
||||
|
||||
const resultList = await VideoPlaylistModel.listForApi({
|
||||
search: req.query.search,
|
||||
followerActorId: serverActor.id,
|
||||
start: req.query.start,
|
||||
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 = [
|
||||
param('playlistId')
|
||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
|
||||
|
@ -354,6 +366,7 @@ export {
|
|||
videoPlaylistsUpdateValidator,
|
||||
videoPlaylistsDeleteValidator,
|
||||
videoPlaylistsGetValidator,
|
||||
videoPlaylistsSearchValidator,
|
||||
|
||||
videoPlaylistsAddVideoValidator,
|
||||
videoPlaylistsUpdateOrRemoveVideoValidator,
|
||||
|
|
|
@ -13,10 +13,11 @@ import {
|
|||
Model,
|
||||
Scopes,
|
||||
Table,
|
||||
UpdatedAt
|
||||
UpdatedAt,
|
||||
Sequelize
|
||||
} from 'sequelize-typescript'
|
||||
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 {
|
||||
isVideoPlaylistDescriptionValid,
|
||||
isVideoPlaylistNameValid,
|
||||
|
@ -67,7 +68,8 @@ type AvailableForListOptions = {
|
|||
type?: VideoPlaylistType
|
||||
accountId?: number
|
||||
videoChannelId?: number
|
||||
privateAndUnlisted?: boolean
|
||||
privateAndUnlisted?: boolean,
|
||||
search?: string
|
||||
}
|
||||
|
||||
@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 = {
|
||||
[Op.and]: whereAnd
|
||||
}
|
||||
|
@ -291,7 +310,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
|||
type?: VideoPlaylistType,
|
||||
accountId?: number,
|
||||
videoChannelId?: number,
|
||||
privateAndUnlisted?: boolean
|
||||
privateAndUnlisted?: boolean,
|
||||
search?: string
|
||||
}) {
|
||||
const query = {
|
||||
offset: options.start,
|
||||
|
@ -308,7 +328,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
|||
followerActorId: options.followerActorId,
|
||||
accountId: options.accountId,
|
||||
videoChannelId: options.videoChannelId,
|
||||
privateAndUnlisted: options.privateAndUnlisted
|
||||
privateAndUnlisted: options.privateAndUnlisted,
|
||||
search: options.search
|
||||
} as AvailableForListOptions
|
||||
]
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue