diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index 1b60260cd..197956cdc 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts @@ -1,8 +1,8 @@ -import { Component, OnInit } from '@angular/core' -import { AuthUser, ScreenService } from '@app/core' -import { TopMenuDropdownParam, TopMenuDropdownComponent } from '../shared/shared-main/misc/top-menu-dropdown.component' -import { RouterOutlet } from '@angular/router' import { NgClass } from '@angular/common' +import { Component, OnInit } from '@angular/core' +import { RouterOutlet } from '@angular/router' +import { AuthUser, PluginService, ScreenService } from '@app/core' +import { TopMenuDropdownComponent, TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdown.component' @Component({ selector: 'my-my-account', @@ -15,17 +15,23 @@ export class MyAccountComponent implements OnInit { menuEntries: TopMenuDropdownParam[] = [] user: AuthUser - constructor (private screenService: ScreenService) { } + constructor ( + private pluginService: PluginService, + private screenService: ScreenService + ) { } get isBroadcastMessageDisplayed () { return this.screenService.isBroadcastMessageDisplayed } ngOnInit (): void { - this.buildMenu() + this.pluginService.ensurePluginsAreLoaded('my-account') + .then(() => this.buildMenu()) } private buildMenu () { + const clientRoutes = this.pluginService.getAllRegisteredClientRoutesForParent('/my-account') || {} + const moderationEntries: TopMenuDropdownParam = { label: $localize`Moderation`, children: [ @@ -68,7 +74,13 @@ export class MyAccountComponent implements OnInit { routerLink: '/my-account/applications' }, - moderationEntries + moderationEntries, + + ...Object.values(clientRoutes) + .map(clientRoute => ({ + label: clientRoute.menuItem?.label, + routerLink: '/my-account/p/' + clientRoute.route + })) ] } } diff --git a/client/src/app/+my-account/routes.ts b/client/src/app/+my-account/routes.ts index 179a96597..d46c42c92 100644 --- a/client/src/app/+my-account/routes.ts +++ b/client/src/app/+my-account/routes.ts @@ -14,6 +14,7 @@ import { BlocklistService } from '@app/shared/shared-moderation/blocklist.servic import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service' import { TwoFactorService } from '@app/shared/shared-users/two-factor.service' import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service' +import { PluginPagesComponent } from '@app/shared/shared-plugin-pages/plugin-pages.component' export default [ { @@ -160,6 +161,19 @@ export default [ title: $localize`Import/Export` } } + }, + { + path: 'p', + children: [ + { + path: '**', + component: PluginPagesComponent, + data: { + parentRoute: '/my-account', + pluginScope: 'my-account' + } + } + ] } ] } diff --git a/client/src/app/+plugin-pages/plugin-pages.component.ts b/client/src/app/+plugin-pages/plugin-pages.component.ts deleted file mode 100644 index a9df10b64..000000000 --- a/client/src/app/+plugin-pages/plugin-pages.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { PluginService } from '@app/core' -import { logger } from '@root-helpers/logger' - -@Component({ - templateUrl: './plugin-pages.component.html', - standalone: true -}) -export class PluginPagesComponent implements AfterViewInit { - @ViewChild('root') root: ElementRef - - constructor ( - private route: ActivatedRoute, - private router: Router, - private pluginService: PluginService - ) { - - } - - ngAfterViewInit () { - this.pluginService.ensurePluginsAreLoaded('common') - .then(() => this.loadRoute()) - } - - private loadRoute () { - const path = '/' + this.route.snapshot.url.map(u => u.path).join('/') - - const registered = this.pluginService.getRegisteredClientRoute(path) - if (!registered) { - logger.info(`Could not find registered route ${path}`, this.pluginService.getAllRegisteredClientRoutes()) - - return this.router.navigate([ '/404' ], { skipLocationChange: true }) - } - - registered.onMount({ rootEl: this.root.nativeElement }) - } -} diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts index adaa860af..90d67e377 100644 --- a/client/src/app/app.routes.ts +++ b/client/src/app/app.routes.ts @@ -62,8 +62,11 @@ const routes: Routes = [ }, { path: 'p', - loadChildren: () => import('./+plugin-pages/routes'), - canActivateChild: [ MetaGuard ] + loadChildren: () => import('./shared/shared-plugin-pages/routes'), + canActivateChild: [ MetaGuard ], + data: { + parentRoute: '/' + } }, { diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index d37b2e5f7..64443dc85 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts @@ -50,7 +50,11 @@ export class PluginService implements ClientHook { video: [] } private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScriptOptions } = {} - private clientRoutes: { [ route: string ]: RegisterClientRouteOptions } = {} + private clientRoutes: { + [ parentRoute in RegisterClientRouteOptions['parentRoute'] ]?: { + [ route: string ]: RegisterClientRouteOptions + } + } = {} private pluginsManager: PluginsManager @@ -126,12 +130,29 @@ export class PluginService implements ClientHook { return this.settingsScripts[npmName] } - getRegisteredClientRoute (route: string) { - return this.clientRoutes[route] + getRegisteredClientRoute (route: string, parentRoute: RegisterClientRouteOptions['parentRoute']) { + if (!this.clientRoutes[parentRoute]) { + return undefined + } + + return this.clientRoutes[parentRoute][route] + } + + getAllRegisteredClientRoutesForParent (parentRoute: RegisterClientRouteOptions['parentRoute']) { + return this.clientRoutes[parentRoute] } getAllRegisteredClientRoutes () { return Object.keys(this.clientRoutes) + .map((parentRoute: RegisterClientRouteOptions['parentRoute']) => { + return Object.keys(this.clientRoutes[parentRoute]) + .map(route => { + if (parentRoute === '/') return route + + return parentRoute + route + }) + }) + .flat() } async translateSetting (npmName: string, setting: RegisterClientFormFieldOptions) { @@ -180,11 +201,17 @@ export class PluginService implements ClientHook { } private onClientRoute (options: RegisterClientRouteOptions) { + const parentRoute = options.parentRoute || '/' + const route = options.route.startsWith('/') ? options.route : `/${options.route}` - this.clientRoutes[route] = options + if (!this.clientRoutes[parentRoute]) { + this.clientRoutes[parentRoute] = {} + } + + this.clientRoutes[parentRoute][route] = options } private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers { diff --git a/client/src/app/shared/shared-plugin-pages/index.ts b/client/src/app/shared/shared-plugin-pages/index.ts new file mode 100644 index 000000000..b0610d9c1 --- /dev/null +++ b/client/src/app/shared/shared-plugin-pages/index.ts @@ -0,0 +1 @@ +export * from './plugin-pages.component' diff --git a/client/src/app/+plugin-pages/plugin-pages.component.html b/client/src/app/shared/shared-plugin-pages/plugin-pages.component.html similarity index 100% rename from client/src/app/+plugin-pages/plugin-pages.component.html rename to client/src/app/shared/shared-plugin-pages/plugin-pages.component.html diff --git a/client/src/app/shared/shared-plugin-pages/plugin-pages.component.ts b/client/src/app/shared/shared-plugin-pages/plugin-pages.component.ts new file mode 100644 index 000000000..2111ac7b7 --- /dev/null +++ b/client/src/app/shared/shared-plugin-pages/plugin-pages.component.ts @@ -0,0 +1,58 @@ +import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { MetaService, PluginService } from '@app/core' +import { logger } from '@root-helpers/logger' +import { Subscription } from 'rxjs/internal/Subscription' + +@Component({ + templateUrl: './plugin-pages.component.html', + standalone: true +}) +export class PluginPagesComponent implements OnDestroy, AfterViewInit { + @ViewChild('root') root: ElementRef + + private urlSub: Subscription + + constructor ( + private metaService: MetaService, + private route: ActivatedRoute, + private router: Router, + private pluginService: PluginService + ) { + + } + + ngAfterViewInit () { + this.urlSub = this.route.url.subscribe(() => { + this.loadRoute() + }) + } + + ngOnDestroy () { + if (this.urlSub) this.urlSub.unsubscribe() + } + + private async loadRoute () { + await this.pluginService.ensurePluginsAreLoaded(this.route.snapshot.data.pluginScope || 'common') + + if (!this.route.snapshot.data.parentRoute) { + logger.error('Missing "parentRoute" URL data to load plugin route ' + this.route.snapshot.url) + return + } + + const path = '/' + this.route.snapshot.url.map(u => u.path).join('/') + + const registered = this.pluginService.getRegisteredClientRoute(path, this.route.snapshot.data.parentRoute) + if (!registered) { + logger.info(`Could not find registered route ${path}`, { routes: this.pluginService.getAllRegisteredClientRoutes() }) + + return this.router.navigate([ '/404' ], { skipLocationChange: true }) + } + + if (registered.title) { + this.metaService.setTitle(registered.title) + } + + registered.onMount({ rootEl: this.root.nativeElement }) + } +} diff --git a/client/src/app/+plugin-pages/routes.ts b/client/src/app/shared/shared-plugin-pages/routes.ts similarity index 100% rename from client/src/app/+plugin-pages/routes.ts rename to client/src/app/shared/shared-plugin-pages/routes.ts diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts index e987f16d6..8c168b0f9 100644 --- a/client/src/root-helpers/plugins-manager.ts +++ b/client/src/root-helpers/plugins-manager.ts @@ -70,7 +70,8 @@ class PluginsManager { 'video-edit': new ReplaySubject(1), 'embed': new ReplaySubject(1), 'my-library': new ReplaySubject(1), - 'video-channel': new ReplaySubject(1) + 'video-channel': new ReplaySubject(1), + 'my-account': new ReplaySubject(1) } private readonly peertubeHelpersFactory: PeertubeHelpersFactory diff --git a/packages/models/src/plugins/client/plugin-client-scope.type.ts b/packages/models/src/plugins/client/plugin-client-scope.type.ts index c09a453b8..56f1ba42d 100644 --- a/packages/models/src/plugins/client/plugin-client-scope.type.ts +++ b/packages/models/src/plugins/client/plugin-client-scope.type.ts @@ -8,4 +8,5 @@ export type PluginClientScope = 'video-edit' | 'admin-plugin' | 'my-library' | - 'video-channel' + 'video-channel' | + 'my-account' diff --git a/packages/models/src/plugins/client/register-client-route.model.ts b/packages/models/src/plugins/client/register-client-route.model.ts index 271b67834..168e32c45 100644 --- a/packages/models/src/plugins/client/register-client-route.model.ts +++ b/packages/models/src/plugins/client/register-client-route.model.ts @@ -1,6 +1,15 @@ export interface RegisterClientRouteOptions { route: string + // Plugin route can be injected in a sub router, like the my-account page + parentRoute?: '/' | '/my-account' + // If parent route has a sub menu, specify the new entry sub menu settings + menuItem?: { + label?: string + } + + title?: string + onMount (options: { rootEl: HTMLElement }): void diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md index 3f85c5791..8f2ddcbbc 100644 --- a/support/doc/plugins/guide.md +++ b/support/doc/plugins/guide.md @@ -872,6 +872,11 @@ To create a client page, register a new client route: function register ({ registerClientRoute }) { registerClientRoute({ route: 'my-super/route', + title: 'Page title for this route', + parentRoute: '/my-account', // Optional. The full path will be /my-account/p/my-super/route. + menuItem: { // Optional. This will add a menu item to this route. Only supported when parentRoute is '/my-account'. + label: 'Sub route', + }, onMount: ({ rootEl }) => { rootEl.innerHTML = 'hello' }