Add client hooks

pull/1987/head
Chocobozzz 2019-07-22 15:40:13 +02:00 committed by Chocobozzz
parent 587568e1cc
commit 93cae47925
31 changed files with 339 additions and 124 deletions

View File

@ -68,7 +68,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
switchMap(res => from(res.data)),
concatMap(videoChannel => {
return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort)
.pipe(map(data => ({ videoChannel, videos: data.videos })))
.pipe(map(data => ({ videoChannel, videos: data.data })))
})
)
.subscribe(({ videoChannel, videos }) => {

View File

@ -69,8 +69,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
return this.videoService
.getAccountVideos(this.account, newPagination, this.sort)
.pipe(
tap(({ totalVideos }) => {
this.titlePage = this.i18n('Published {{totalVideos}} videos', { totalVideos })
tap(({ total }) => {
this.titlePage = this.i18n('Published {{total}} videos', { total })
})
)
}

View File

@ -131,9 +131,9 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
private loadElements () {
this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
.subscribe(({ totalVideos, videos }) => {
this.videos = this.videos.concat(videos)
this.pagination.totalItems = totalVideos
.subscribe(({ total, data }) => {
this.videos = this.videos.concat(data)
this.pagination.totalItems = total
})
}

View File

@ -71,8 +71,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
return this.videoService
.getVideoChannelVideos(this.videoChannel, newPagination, this.sort)
.pipe(
tap(({ totalVideos }) => {
this.titlePage = this.i18n('Published {{totalVideos}} videos', { totalVideos })
tap(({ total }) => {
this.titlePage = this.i18n('Published {{total}} videos', { total })
})
)
}

View File

@ -10,6 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
import { fromEvent } from 'rxjs'
import { ViewportScroller } from '@angular/common'
import { PluginService } from '@app/core/plugins/plugin.service'
import { HooksService } from '@app/core/plugins/hooks.service'
@Component({
selector: 'my-app',
@ -33,7 +34,8 @@ export class AppComponent implements OnInit {
private redirectService: RedirectService,
private screenService: ScreenService,
private hotkeysService: HotkeysService,
private themeService: ThemeService
private themeService: ThemeService,
private hooks: HooksService
) { }
get serverVersion () {
@ -206,7 +208,7 @@ export class AppComponent implements OnInit {
await this.pluginService.loadPluginsByScope('common')
this.pluginService.runHook('action:application.loaded')
this.hooks.runAction('action:application.init')
}
private initHotkeys () {

View File

@ -22,6 +22,7 @@ import { UserNotificationSocket } from '@app/core/notification/user-notification
import { ServerConfigResolver } from './routing/server-config-resolver.service'
import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
import { PluginService } from '@app/core/plugins/plugin.service'
import { HooksService } from '@app/core/plugins/hooks.service'
@NgModule({
imports: [
@ -63,6 +64,7 @@ import { PluginService } from '@app/core/plugins/plugin.service'
UnloggedGuard,
PluginService,
HooksService,
RedirectService,
Notifier,

View File

@ -0,0 +1,44 @@
import { Injectable } from '@angular/core'
import { PluginService } from '@app/core/plugins/plugin.service'
import { ClientActionHookName, ClientFilterHookName } from '@shared/models/plugins/client-hook.model'
import { from, Observable } from 'rxjs'
import { mergeMap, switchMap } from 'rxjs/operators'
import { ServerService } from '@app/core/server'
import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type'
type RawFunction<U, T> = (params: U) => T
type ObservableFunction<U, T> = RawFunction<U, Observable<T>>
@Injectable()
export class HooksService {
constructor (
private server: ServerService,
private pluginService: PluginService
) { }
wrapObject<T, U extends ClientFilterHookName> (result: T, hookName: U) {
return this.pluginService.runHook(hookName, result)
}
wrapObsFun
<P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName>
(fun: ObservableFunction<P, R>, params: P, scope: PluginClientScope, hookParamName: H1, hookResultName: H2) {
return from(this.pluginService.ensurePluginsAreLoaded(scope))
.pipe(
mergeMap(() => this.wrapObject(params, hookParamName)),
switchMap(params => fun(params)),
mergeMap(result => this.pluginService.runHook(hookResultName, result, params))
)
}
async wrapFun<U, T, V extends ClientFilterHookName> (fun: RawFunction<U, T>, params: U, hookName: V) {
const result = fun(params)
return this.pluginService.runHook(hookName, result, params)
}
runAction<T, U extends ClientActionHookName> (hookName: U, params?: T) {
this.pluginService.runHook(hookName, params)
.catch((err: any) => console.error('Fatal hook error.', { err }))
}
}

View File

@ -3,11 +3,13 @@ import { Router } from '@angular/router'
import { ServerConfigPlugin } from '@shared/models'
import { ServerService } from '@app/core/server/server.service'
import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
import { PluginScope } from '@shared/models/plugins/plugin-scope.type'
import { environment } from '../../../environments/environment'
import { RegisterHookOptions } from '@shared/models/plugins/register-hook.model'
import { ReplaySubject } from 'rxjs'
import { first, shareReplay } from 'rxjs/operators'
import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
import { ClientHook, ClientHookName } from '@shared/models/plugins/client-hook.model'
import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type'
interface HookStructValue extends RegisterHookOptions {
plugin: ServerConfigPlugin
@ -21,14 +23,18 @@ type PluginInfo = {
}
@Injectable()
export class PluginService {
pluginsLoaded = new ReplaySubject<boolean>(1)
export class PluginService implements ClientHook {
pluginsBuilt = new ReplaySubject<boolean>(1)
pluginsLoaded: { [ scope in PluginClientScope ]: ReplaySubject<boolean> } = {
common: new ReplaySubject<boolean>(1),
'video-watch': new ReplaySubject<boolean>(1)
}
private plugins: ServerConfigPlugin[] = []
private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
private loadedPlugins: { [ name: string ]: boolean } = {}
private loadedScripts: { [ script: string ]: boolean } = {}
private loadedScopes: PluginScope[] = []
private loadedScopes: PluginClientScope[] = []
private hooks: { [ name: string ]: HookStructValue[] } = {}
@ -45,12 +51,18 @@ export class PluginService {
this.buildScopeStruct()
this.pluginsLoaded.next(true)
this.pluginsBuilt.next(true)
})
}
ensurePluginsAreLoaded () {
return this.pluginsLoaded.asObservable()
ensurePluginsAreBuilt () {
return this.pluginsBuilt.asObservable()
.pipe(first(), shareReplay())
.toPromise()
}
ensurePluginsAreLoaded (scope: PluginClientScope) {
return this.pluginsLoaded[scope].asObservable()
.pipe(first(), shareReplay())
.toPromise()
}
@ -90,9 +102,9 @@ export class PluginService {
}
}
async loadPluginsByScope (scope: PluginScope, isReload = false) {
async loadPluginsByScope (scope: PluginClientScope, isReload = false) {
try {
await this.ensurePluginsAreLoaded()
await this.ensurePluginsAreBuilt()
if (!isReload) this.loadedScopes.push(scope)
@ -111,32 +123,24 @@ export class PluginService {
}
await Promise.all(promises)
this.pluginsLoaded[scope].next(true)
} catch (err) {
console.error('Cannot load plugins by scope %s.', scope, err)
}
}
async runHook (hookName: string, param?: any) {
let result = param
async runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
if (!this.hooks[hookName]) return Promise.resolve(result)
if (!this.hooks[hookName]) return result
const wait = hookName.startsWith('static:')
const hookType = getHookType(hookName)
for (const hook of this.hooks[hookName]) {
try {
const p = hook.handler(param)
console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
if (wait) {
result = await p
} else if (p.catch) {
p.catch((err: Error) => {
console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err)
})
}
} catch (err) {
console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err)
}
result = await internalRunHook(hook.handler, hookType, result, params, err => {
console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
})
}
return result

View File

@ -10,6 +10,7 @@ import { AdvancedSearch } from '@app/search/advanced-search.model'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { immutableAssign } from '@app/shared/misc/utils'
import { Video } from '@app/shared/video/video.model'
import { HooksService } from '@app/core/plugins/hooks.service'
@Component({
selector: 'my-search',
@ -41,7 +42,8 @@ export class SearchComponent implements OnInit, OnDestroy {
private metaService: MetaService,
private notifier: Notifier,
private searchService: SearchService,
private authService: AuthService
private authService: AuthService,
private hooks: HooksService
) { }
get user () {
@ -93,18 +95,18 @@ export class SearchComponent implements OnInit, OnDestroy {
search () {
forkJoin([
this.searchService.searchVideos(this.currentSearch, this.pagination, this.advancedSearch),
this.searchService.searchVideoChannels(this.currentSearch, immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }))
this.getVideosObs(),
this.getVideoChannelObs()
])
.subscribe(
([ videosResult, videoChannelsResult ]) => {
this.results = this.results
.concat(videoChannelsResult.data)
.concat(videosResult.videos)
this.pagination.totalItems = videosResult.totalVideos + videoChannelsResult.total
.concat(videosResult.data)
this.pagination.totalItems = videosResult.total + videoChannelsResult.total
// Focus on channels if there are no enough videos
if (this.firstSearch === true && videosResult.videos.length < this.pagination.itemsPerPage) {
if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) {
this.resetPagination()
this.firstSearch = false
@ -117,7 +119,6 @@ export class SearchComponent implements OnInit, OnDestroy {
err => this.notifier.error(err.message)
)
}
onNearOfBottom () {
@ -163,4 +164,35 @@ export class SearchComponent implements OnInit, OnDestroy {
queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search })
})
}
private getVideosObs () {
const params = {
search: this.currentSearch,
componentPagination: this.pagination,
advancedSearch: this.advancedSearch
}
return this.hooks.wrapObsFun(
this.searchService.searchVideos.bind(this.searchService),
params,
'common',
'filter:api.search.videos.list.params',
'filter:api.search.videos.list.result'
)
}
private getVideoChannelObs () {
const params = {
search: this.currentSearch,
componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage })
}
return this.hooks.wrapObsFun(
this.searchService.searchVideoChannels.bind(this.searchService),
params,
'common',
'filter:api.search.video-channels.list.params',
'filter:api.search.video-channels.list.result'
)
}
}

View File

@ -23,13 +23,14 @@ export class SearchService {
private videoService: VideoService
) {}
searchVideos (
searchVideos (parameters: {
search: string,
componentPagination: ComponentPagination,
advancedSearch: AdvancedSearch
): Observable<{ videos: Video[], totalVideos: number }> {
const url = SearchService.BASE_SEARCH_URL + 'videos'
}): Observable<ResultList<Video>> {
const { search, componentPagination, advancedSearch } = parameters
const url = SearchService.BASE_SEARCH_URL + 'videos'
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
let params = new HttpParams()
@ -48,12 +49,13 @@ export class SearchService {
)
}
searchVideoChannels (
searchVideoChannels (parameters: {
search: string,
componentPagination: ComponentPagination
): Observable<{ data: VideoChannel[], total: number }> {
const url = SearchService.BASE_SEARCH_URL + 'video-channels'
}): Observable<ResultList<VideoChannel>> {
const { search, componentPagination } = parameters
const url = SearchService.BASE_SEARCH_URL + 'video-channels'
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
let params = new HttpParams()

View File

@ -45,7 +45,7 @@ export class OverviewService {
of(object.videos)
.pipe(
switchMap(videos => this.videosService.extractVideos({ total: 0, data: videos })),
map(result => result.videos),
map(result => result.data),
tap(videos => {
videosOverviewResult[key].push(immutableAssign(object, { videos }))
})

View File

@ -13,6 +13,7 @@ import { Notifier, ServerService } from '@app/core'
import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date'
import { ResultList } from '@shared/models'
enum GroupDate {
UNKNOWN = 0,
@ -73,7 +74,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
private groupedDateLabels: { [id in GroupDate]: string }
private groupedDates: { [id: number]: GroupDate } = {}
abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }>
abstract getVideosObservable (page: number): Observable<ResultList<Video>>
abstract generateSyndicationList (): void
@ -138,12 +139,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
}
loadMoreVideos () {
const observable = this.getVideosObservable(this.pagination.currentPage)
observable.subscribe(
({ videos, totalVideos }) => {
this.pagination.totalItems = totalVideos
this.videos = this.videos.concat(videos)
this.getVideosObservable(this.pagination.currentPage).subscribe(
({ data, total }) => {
this.pagination.totalItems = total
this.videos = this.videos.concat(data)
if (this.groupByDate) this.buildGroupedDateLabels()

View File

@ -41,7 +41,7 @@ export interface VideosProvider {
filter?: VideoFilter,
categoryOneOf?: number,
languageOneOf?: string[]
}): Observable<{ videos: Video[], totalVideos: number }>
}): Observable<ResultList<Video>>
}
@Injectable()
@ -65,11 +65,11 @@ export class VideoService implements VideosProvider {
return VideoService.BASE_VIDEO_URL + uuid + '/watching'
}
getVideo (uuid: string): Observable<VideoDetails> {
getVideo (options: { videoId: string }): Observable<VideoDetails> {
return this.serverService.localeObservable
.pipe(
switchMap(translations => {
return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid)
return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + options.videoId)
.pipe(map(videoHash => ({ videoHash, translations })))
}),
map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)),
@ -123,7 +123,7 @@ export class VideoService implements VideosProvider {
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number }> {
getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<ResultList<Video>> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
@ -141,7 +141,7 @@ export class VideoService implements VideosProvider {
account: Account,
videoPagination: ComponentPagination,
sort: VideoSortField
): Observable<{ videos: Video[], totalVideos: number }> {
): Observable<ResultList<Video>> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
@ -159,7 +159,7 @@ export class VideoService implements VideosProvider {
videoChannel: VideoChannel,
videoPagination: ComponentPagination,
sort: VideoSortField
): Observable<{ videos: Video[], totalVideos: number }> {
): Observable<ResultList<Video>> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
@ -176,7 +176,7 @@ export class VideoService implements VideosProvider {
getPlaylistVideos (
videoPlaylistId: number | string,
videoPagination: ComponentPagination
): Observable<{ videos: Video[], totalVideos: number }> {
): Observable<ResultList<Video>> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
@ -190,10 +190,11 @@ export class VideoService implements VideosProvider {
)
}
getUserSubscriptionVideos (
getUserSubscriptionVideos (parameters: {
videoPagination: ComponentPagination,
sort: VideoSortField
): Observable<{ videos: Video[], totalVideos: number }> {
}): Observable<ResultList<Video>> {
const { videoPagination, sort } = parameters
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
@ -213,7 +214,7 @@ export class VideoService implements VideosProvider {
filter?: VideoFilter,
categoryOneOf?: number,
languageOneOf?: string[]
}): Observable<{ videos: Video[], totalVideos: number }> {
}): Observable<ResultList<Video>> {
const { videoPagination, sort, filter, categoryOneOf, languageOneOf } = parameters
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
@ -344,7 +345,7 @@ export class VideoService implements VideosProvider {
videos.push(new Video(videoJson, translations))
}
return { videos, totalVideos }
return { total: totalVideos, data: videos }
})
)
}

View File

@ -21,6 +21,7 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
import { VideoSortField } from '@app/shared/video/sort-field.type'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ResultList } from '@shared/models'
export type SelectionType = { [ id: number ]: boolean }
@ -33,7 +34,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
@Input() pagination: ComponentPagination
@Input() titlePage: string
@Input() miniatureDisplayOptions: MiniatureDisplayOptions
@Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<{ videos: Video[], totalVideos: number }>
@Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
@ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective>
@Output() selectionChange = new EventEmitter<SelectionType>()

View File

@ -18,7 +18,7 @@ export class VideoUpdateResolver implements Resolve<any> {
resolve (route: ActivatedRouteSnapshot) {
const uuid: string = route.params[ 'uuid' ]
return this.videoService.getVideo(uuid)
return this.videoService.getVideo({ videoId: uuid })
.pipe(
switchMap(video => {
return forkJoin([

View File

@ -48,11 +48,13 @@ export class VideoCommentService {
)
}
getVideoCommentThreads (
getVideoCommentThreads (parameters: {
videoId: number | string,
componentPagination: ComponentPagination,
sort: VideoSortField
): Observable<{ comments: VideoComment[], totalComments: number}> {
}): Observable<{ comments: VideoComment[], totalComments: number}> {
const { videoId, componentPagination, sort } = parameters
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
let params = new HttpParams()
@ -67,7 +69,11 @@ export class VideoCommentService {
)
}
getVideoThreadComments (videoId: number | string, threadId: number): Observable<VideoCommentThreadTree> {
getVideoThreadComments (parameters: {
videoId: number | string,
threadId: number
}): Observable<VideoCommentThreadTree> {
const { videoId, threadId } = parameters
const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}`
return this.authHttp

View File

@ -12,6 +12,7 @@ import { VideoComment } from './video-comment.model'
import { VideoCommentService } from './video-comment.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Syndication } from '@app/shared/video/syndication.model'
import { HooksService } from '@app/core/plugins/hooks.service'
@Component({
selector: 'my-video-comments',
@ -45,7 +46,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
private confirmService: ConfirmService,
private videoCommentService: VideoCommentService,
private activatedRoute: ActivatedRoute,
private i18n: I18n
private i18n: I18n,
private hooks: HooksService
) {}
ngOnInit () {
@ -73,8 +75,20 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
viewReplies (commentId: number, highlightThread = false) {
this.threadLoading[commentId] = true
this.videoCommentService.getVideoThreadComments(this.video.id, commentId)
.subscribe(
const params = {
videoId: this.video.id,
threadId: commentId
}
const obs = this.hooks.wrapObsFun(
this.videoCommentService.getVideoThreadComments.bind(this.videoCommentService),
params,
'video-watch',
'filter:api.video-watch.video-thread-replies.list.params',
'filter:api.video-watch.video-thread-replies.list.result'
)
obs.subscribe(
res => {
this.threadComments[commentId] = res
this.threadLoading[commentId] = false
@ -91,16 +105,29 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
)
}
loadMoreComments () {
this.videoCommentService.getVideoCommentThreads(this.video.id, this.componentPagination, this.sort)
.subscribe(
res => {
this.comments = this.comments.concat(res.comments)
this.componentPagination.totalItems = res.totalComments
},
loadMoreThreads () {
const params = {
videoId: this.video.id,
componentPagination: this.componentPagination,
sort: this.sort
}
err => this.notifier.error(err.message)
)
const obs = this.hooks.wrapObsFun(
this.videoCommentService.getVideoCommentThreads.bind(this.videoCommentService),
params,
'video-watch',
'filter:api.video-watch.video-threads.list.params',
'filter:api.video-watch.video-threads.list.result'
)
obs.subscribe(
res => {
this.comments = this.comments.concat(res.comments)
this.componentPagination.totalItems = res.totalComments
},
err => this.notifier.error(err.message)
)
}
onCommentThreadCreated (comment: VideoComment) {
@ -169,7 +196,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
this.componentPagination.currentPage++
if (hasMoreItems(this.componentPagination)) {
this.loadMoreComments()
this.loadMoreThreads()
}
}
@ -197,7 +224,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid)
this.loadMoreComments()
this.loadMoreThreads()
}
}

View File

@ -66,11 +66,11 @@ export class VideoWatchPlaylistComponent {
loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) {
this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination)
.subscribe(({ totalVideos, videos }) => {
this.playlistVideos = this.playlistVideos.concat(videos)
this.playlistPagination.totalItems = totalVideos
.subscribe(({ total, data }) => {
this.playlistVideos = this.playlistVideos.concat(data)
this.playlistPagination.totalItems = total
if (totalVideos === 0) {
if (total === 0) {
this.noPlaylistVideos = true
return
}

View File

@ -33,6 +33,7 @@ import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage'
import { PluginService } from '@app/core/plugins/plugin.service'
import { HooksService } from '@app/core/plugins/hooks.service'
@Component({
selector: 'my-video-watch',
@ -93,6 +94,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private videoCaptionService: VideoCaptionService,
private i18n: I18n,
private hotkeysService: HotkeysService,
private hooks: HooksService,
@Inject(LOCALE_ID) private localeId: string
) {}
@ -131,7 +133,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.theaterEnabled = getStoredTheater()
this.pluginService.runHook('action:video-watch.loaded')
this.hooks.runAction('action:video-watch.init')
}
ngOnDestroy () {
@ -246,9 +248,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (this.player) this.player.pause()
const videoObs = this.hooks.wrapObsFun(
this.videoService.getVideo.bind(this.videoService),
{ videoId },
'video-watch',
'filter:api.video-watch.video.get.params',
'filter:api.video-watch.video.get.result'
)
// Video did change
forkJoin(
this.videoService.getVideo(videoId),
videoObs,
this.videoCaptionService.listCaptions(videoId)
)
.pipe(
@ -486,6 +496,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.setOpenGraphTags()
this.checkUserRating()
this.hooks.runAction('action:video-watch.video.loaded')
}
private setRating (nextRating: UserVideoRateType) {

View File

@ -33,20 +33,24 @@ export class RecentVideosRecommendationService implements RecommendationService
private fetchPage (page: number, recommendation: RecommendationInfo): Observable<Video[]> {
const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 }
const defaultSubscription = this.videos.getVideos({ videoPagination: pagination, sort: '-createdAt' })
.pipe(map(v => v.videos))
.pipe(map(v => v.data))
if (!recommendation.tags || recommendation.tags.length === 0) return defaultSubscription
return this.searchService.searchVideos('',
pagination,
new AdvancedSearch({ tagsOneOf: recommendation.tags.join(','), sort: '-createdAt' })
).pipe(
map(v => v.videos),
switchMap(videos => {
if (videos.length <= 1) return defaultSubscription
const params = {
search: '',
componentPagination: pagination,
advancedSearch: new AdvancedSearch({ tagsOneOf: recommendation.tags.join(','), sort: '-createdAt' })
}
return of(videos)
})
)
return this.searchService.searchVideos(params)
.pipe(
map(v => v.data),
switchMap(videos => {
if (videos.length <= 1) return defaultSubscription
return of(videos)
})
)
}
}

View File

@ -10,6 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
import { UserRight } from '../../../../../shared/models/users'
import { Notifier, ServerService } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
@Component({
selector: 'my-videos-local',
@ -31,7 +32,8 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
protected notifier: Notifier,
protected authService: AuthService,
protected screenService: ScreenService,
private videoService: VideoService
private videoService: VideoService,
private hooks: HooksService
) {
super()
@ -55,14 +57,21 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
return this.videoService.getVideos({
const params = {
videoPagination: newPagination,
sort: this.sort,
filter: this.filter,
categoryOneOf: this.categoryOneOf,
languageOneOf: this.languageOneOf
})
}
return this.hooks.wrapObsFun(
this.videoService.getVideos.bind(this.videoService),
params,
'common',
'filter:api.videos.list.local.params',
'filter:api.videos.list.local.result'
)
}
generateSyndicationList () {

View File

@ -8,6 +8,7 @@ import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
import { Notifier, ServerService } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
@Component({
selector: 'my-videos-recently-added',
@ -29,7 +30,8 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
protected notifier: Notifier,
protected authService: AuthService,
protected screenService: ScreenService,
private videoService: VideoService
private videoService: VideoService,
private hooks: HooksService
) {
super()
@ -48,14 +50,20 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
return this.videoService.getVideos({
const params = {
videoPagination: newPagination,
sort: this.sort,
filter: undefined,
categoryOneOf: this.categoryOneOf,
languageOneOf: this.languageOneOf
})
}
return this.hooks.wrapObsFun(
this.videoService.getVideos.bind(this.videoService),
params,
'common',
'filter:api.videos.list.recently-added.params',
'filter:api.videos.list.recently-added.result'
)
}
generateSyndicationList () {

View File

@ -8,6 +8,7 @@ import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
import { Notifier, ServerService } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
@Component({
selector: 'my-videos-trending',
@ -28,7 +29,8 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
protected notifier: Notifier,
protected authService: AuthService,
protected screenService: ScreenService,
private videoService: VideoService
private videoService: VideoService,
private hooks: HooksService
) {
super()
}
@ -61,13 +63,20 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
return this.videoService.getVideos({
const params = {
videoPagination: newPagination,
sort: this.sort,
filter: undefined,
categoryOneOf: this.categoryOneOf,
languageOneOf: this.languageOneOf
})
}
return this.hooks.wrapObsFun(
this.videoService.getVideos.bind(this.videoService),
params,
'common',
'filter:api.videos.list.trending.params',
'filter:api.videos.list.trending.result'
)
}
generateSyndicationList () {

View File

@ -9,6 +9,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
import { Notifier, ServerService } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
@Component({
selector: 'my-videos-user-subscriptions',
@ -29,7 +30,8 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
protected notifier: Notifier,
protected authService: AuthService,
protected screenService: ScreenService,
private videoService: VideoService
private videoService: VideoService,
private hooks: HooksService
) {
super()
@ -46,8 +48,18 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
const params = {
videoPagination: newPagination,
sort: this.sort
}
return this.videoService.getUserSubscriptionVideos(newPagination, this.sort)
return this.hooks.wrapObsFun(
this.videoService.getUserSubscriptionVideos.bind(this.videoService),
params,
'common',
'filter:api.videos.list.user-subscriptions.params',
'filter:api.videos.list.user-subscriptions.result'
)
}
generateSyndicationList () {

View File

@ -12,6 +12,7 @@
"noImplicitThis": true,
"suppressImplicitAnyIndexErrors":true,
"alwaysStrict": true,
"strictBindCallApply": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"

View File

@ -29,7 +29,7 @@ async function internalRunHook <T> (handler: Function, hookType: HookType, resul
}
if (hookType === HookType.ACTION) {
if (isCatchable(p)) p.catch(err => onError(err))
if (isCatchable(p)) p.catch((err: any) => onError(err))
return undefined
}

View File

@ -0,0 +1,39 @@
export type ClientFilterHookName =
'filter:api.videos.list.trending.params' |
'filter:api.videos.list.trending.result' |
'filter:api.videos.list.local.params' |
'filter:api.videos.list.local.result' |
'filter:api.videos.list.recently-added.params' |
'filter:api.videos.list.recently-added.result' |
'filter:api.videos.list.user-subscriptions.params' |
'filter:api.videos.list.user-subscriptions.result' |
'filter:api.video-watch.video.get.params' |
'filter:api.video-watch.video.get.result' |
'filter:api.video-watch.video-threads.list.params' |
'filter:api.video-watch.video-threads.list.result' |
'filter:api.video-watch.video-thread-replies.list.params' |
'filter:api.video-watch.video-thread-replies.list.result' |
'filter:api.search.videos.list.params' |
'filter:api.search.videos.list.result' |
'filter:api.search.video-channels.list.params' |
'filter:api.search.video-channels.list.result'
export type ClientActionHookName =
'action:application.init' |
'action:video-watch.init' |
'action:video-watch.video.loaded'
export type ClientHookName = ClientActionHookName | ClientFilterHookName
export interface ClientHook {
runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T>
}

View File

@ -0,0 +1 @@
export type PluginClientScope = 'common' | 'video-watch'

View File

@ -1 +0,0 @@
export type PluginScope = 'common' | 'video-watch'

View File

@ -30,5 +30,5 @@ export type ServerActionHookName =
export type ServerHookName = ServerFilterHookName | ServerActionHookName
export interface ServerHook {
runHook (hookName: ServerHookName, params?: any)
runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T>
}

View File

@ -1,11 +1,12 @@
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
import { ClientScript } from '../plugins/plugin-package-json.model'
import { PluginClientScope } from '../plugins/plugin-scope.type'
export interface ServerConfigPlugin {
name: string
version: string
description: string
clientScripts: { [name: string]: ClientScript }
clientScripts: { [name in PluginClientScope]: ClientScript }
}
export interface ServerConfigTheme extends ServerConfigPlugin {