diff options
-rw-r--r-- | client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts | 1 | ||||
-rw-r--r-- | client/src/app/core/menu/menu.service.ts | 106 | ||||
-rw-r--r-- | client/src/app/menu/menu.component.html | 41 | ||||
-rw-r--r-- | client/src/app/menu/menu.component.scss | 3 | ||||
-rw-r--r-- | client/src/app/menu/menu.component.ts | 73 | ||||
-rw-r--r-- | shared/models/plugins/client/client-hook.model.ts | 5 | ||||
-rw-r--r-- | support/doc/plugins/guide.md | 6 |
7 files changed, 135 insertions, 100 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts index bac1015fc..671e734ac 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts | |||
@@ -82,6 +82,7 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges { | |||
82 | 82 | ||
83 | buildLandingPageOptions () { | 83 | buildLandingPageOptions () { |
84 | this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig) | 84 | this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig) |
85 | .links | ||
85 | .map(o => ({ | 86 | .map(o => ({ |
86 | id: o.path, | 87 | id: o.path, |
87 | label: o.label, | 88 | label: o.label, |
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts index a30766b29..60130382f 100644 --- a/client/src/app/core/menu/menu.service.ts +++ b/client/src/app/core/menu/menu.service.ts | |||
@@ -2,16 +2,23 @@ import { fromEvent } from 'rxjs' | |||
2 | import { debounceTime } from 'rxjs/operators' | 2 | import { debounceTime } from 'rxjs/operators' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { GlobalIconName } from '@app/shared/shared-icons' | 4 | import { GlobalIconName } from '@app/shared/shared-icons' |
5 | import { sortObjectComparator } from '@shared/core-utils/miscs/miscs' | ||
6 | import { HTMLServerConfig } from '@shared/models/server' | 5 | import { HTMLServerConfig } from '@shared/models/server' |
7 | import { ScreenService } from '../wrappers' | 6 | import { ScreenService } from '../wrappers' |
8 | 7 | ||
9 | export type MenuLink = { | 8 | export type MenuLink = { |
10 | icon: GlobalIconName | 9 | icon: GlobalIconName |
10 | |||
11 | label: string | 11 | label: string |
12 | menuLabel: string | 12 | // Used by the left menu for example |
13 | shortLabel: string | ||
14 | |||
13 | path: string | 15 | path: string |
14 | priority: number | 16 | } |
17 | |||
18 | export type MenuSection = { | ||
19 | key: string | ||
20 | title: string | ||
21 | links: MenuLink[] | ||
15 | } | 22 | } |
16 | 23 | ||
17 | @Injectable() | 24 | @Injectable() |
@@ -59,51 +66,90 @@ export class MenuService { | |||
59 | this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser | 66 | this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser |
60 | } | 67 | } |
61 | 68 | ||
62 | buildCommonLinks (config: HTMLServerConfig) { | 69 | buildLibraryLinks (userCanSeeVideosLink: boolean): MenuSection { |
63 | let entries: MenuLink[] = [ | 70 | let links: MenuLink[] = [] |
71 | |||
72 | if (userCanSeeVideosLink) { | ||
73 | links.push({ | ||
74 | path: '/my-library/videos', | ||
75 | icon: 'videos' as GlobalIconName, | ||
76 | shortLabel: $localize`Videos`, | ||
77 | label: $localize`My videos` | ||
78 | }) | ||
79 | } | ||
80 | |||
81 | links = links.concat([ | ||
82 | { | ||
83 | path: '/my-library/video-playlists', | ||
84 | icon: 'playlists' as GlobalIconName, | ||
85 | shortLabel: $localize`Playlists`, | ||
86 | label: $localize`My playlists` | ||
87 | }, | ||
88 | { | ||
89 | path: '/videos/subscriptions', | ||
90 | icon: 'subscriptions' as GlobalIconName, | ||
91 | shortLabel: $localize`Subscriptions`, | ||
92 | label: $localize`My subscriptions` | ||
93 | }, | ||
94 | { | ||
95 | path: '/my-library/history/videos', | ||
96 | icon: 'history' as GlobalIconName, | ||
97 | shortLabel: $localize`History`, | ||
98 | label: $localize`My history` | ||
99 | } | ||
100 | ]) | ||
101 | |||
102 | return { | ||
103 | key: 'in-my-library', | ||
104 | title: 'In my library', | ||
105 | links | ||
106 | } | ||
107 | } | ||
108 | |||
109 | buildCommonLinks (config: HTMLServerConfig): MenuSection { | ||
110 | let links: MenuLink[] = [] | ||
111 | |||
112 | if (config.homepage.enabled) { | ||
113 | links.push({ | ||
114 | icon: 'home' as 'home', | ||
115 | label: $localize`Home`, | ||
116 | shortLabel: $localize`Home`, | ||
117 | path: '/home' | ||
118 | }) | ||
119 | } | ||
120 | |||
121 | links = links.concat([ | ||
64 | { | 122 | { |
65 | icon: 'globe' as 'globe', | 123 | icon: 'globe' as 'globe', |
66 | label: $localize`Discover videos`, | 124 | label: $localize`Discover videos`, |
67 | menuLabel: $localize`Discover`, | 125 | shortLabel: $localize`Discover`, |
68 | path: '/videos/overview', | 126 | path: '/videos/overview' |
69 | priority: 150 | ||
70 | }, | 127 | }, |
71 | { | 128 | { |
72 | icon: 'trending' as 'trending', | 129 | icon: 'trending' as 'trending', |
73 | label: $localize`Trending videos`, | 130 | label: $localize`Trending videos`, |
74 | menuLabel: $localize`Trending`, | 131 | shortLabel: $localize`Trending`, |
75 | path: '/videos/trending', | 132 | path: '/videos/trending' |
76 | priority: 140 | ||
77 | }, | 133 | }, |
78 | { | 134 | { |
79 | icon: 'recently-added' as 'recently-added', | 135 | icon: 'recently-added' as 'recently-added', |
80 | label: $localize`Recently added videos`, | 136 | label: $localize`Recently added videos`, |
81 | menuLabel: $localize`Recently added`, | 137 | shortLabel: $localize`Recently added`, |
82 | path: '/videos/recently-added', | 138 | path: '/videos/recently-added' |
83 | priority: 130 | ||
84 | }, | 139 | }, |
85 | { | 140 | { |
86 | icon: 'local' as 'local', | 141 | icon: 'local' as 'local', |
87 | label: $localize`Local videos`, | 142 | label: $localize`Local videos`, |
88 | menuLabel: $localize`Local videos`, | 143 | shortLabel: $localize`Local videos`, |
89 | path: '/videos/local', | 144 | path: '/videos/local' |
90 | priority: 120 | ||
91 | } | 145 | } |
92 | ] | 146 | ]) |
93 | 147 | ||
94 | if (config.homepage.enabled) { | 148 | return { |
95 | entries.push({ | 149 | key: 'on-instance', |
96 | icon: 'home' as 'home', | 150 | title: $localize`ON ${config.instance.name}`, |
97 | label: $localize`Home`, | 151 | links |
98 | menuLabel: $localize`Home`, | ||
99 | path: '/home', | ||
100 | priority: 160 | ||
101 | }) | ||
102 | } | 152 | } |
103 | |||
104 | entries = entries.sort(sortObjectComparator('priority', 'desc')) | ||
105 | |||
106 | return entries | ||
107 | } | 153 | } |
108 | 154 | ||
109 | private handleWindowResize () { | 155 | private handleWindowResize () { |
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 099ee8f36..16c79efc1 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -95,39 +95,16 @@ | |||
95 | <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> | 95 | <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> |
96 | </div> | 96 | </div> |
97 | 97 | ||
98 | <div *ngIf="isLoggedIn" class="in-my-library"> | 98 | <ng-container *ngFor="let menuSection of menuSections" > |
99 | <div i18n class="block-title">IN MY LIBRARY</div> | 99 | <div [ngClass]="[ menuSection.key, 'menu-block' ]"> |
100 | <div i18n class="block-title">{{ menuSection.title }}</div> | ||
100 | 101 | ||
101 | <a *ngIf="user.canSeeVideosLink" class="menu-link" routerLink="/my-library/videos" routerLinkActive="active"> | 102 | <a class="menu-link" *ngFor="let link of menuSection.links" [routerLink]="link.path" routerLinkActive="active"> |
102 | <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> | 103 | <my-global-icon [iconName]="link.icon" aria-hidden="true"></my-global-icon> |
103 | <ng-container i18n>Videos</ng-container> | 104 | <ng-container>{{ link.shortLabel }}</ng-container> |
104 | </a> | 105 | </a> |
105 | 106 | </div> | |
106 | <a class="menu-link" routerLink="/my-library/video-playlists" routerLinkActive="active"> | 107 | </ng-container> |
107 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> | ||
108 | <ng-container i18n>Playlists</ng-container> | ||
109 | </a> | ||
110 | |||
111 | <a class="menu-link" routerLink="/videos/subscriptions" routerLinkActive="active"> | ||
112 | <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon> | ||
113 | <ng-container i18n>Subscriptions</ng-container> | ||
114 | </a> | ||
115 | |||
116 | <a class="menu-link" routerLink="/my-library/history/videos" routerLinkActive="active"> | ||
117 | <my-global-icon iconName="history" aria-hidden="true"></my-global-icon> | ||
118 | <ng-container i18n>History</ng-container> | ||
119 | </a> | ||
120 | |||
121 | </div> | ||
122 | |||
123 | <div class="on-instance"> | ||
124 | <div i18n class="block-title">ON {{instanceName}}</div> | ||
125 | |||
126 | <a class="menu-link" *ngFor="let commonLink of commonMenuLinks" [routerLink]="commonLink.path" routerLinkActive="active"> | ||
127 | <my-global-icon [iconName]="commonLink.icon" aria-hidden="true"></my-global-icon> | ||
128 | <ng-container>{{ commonLink.menuLabel }}</ng-container> | ||
129 | </a> | ||
130 | </div> | ||
131 | </div> | 108 | </div> |
132 | 109 | ||
133 | <div class="footer"> | 110 | <div class="footer"> |
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index 6da80191a..daaed2d32 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss | |||
@@ -274,8 +274,7 @@ my-actor-avatar { | |||
274 | } | 274 | } |
275 | } | 275 | } |
276 | 276 | ||
277 | .in-my-library, | 277 | .menu-block, |
278 | .on-instance, | ||
279 | .footer-block { | 278 | .footer-block { |
280 | margin-bottom: 15px; | 279 | margin-bottom: 15px; |
281 | 280 | ||
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index c767f19b2..fa104cc43 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -8,7 +8,8 @@ import { | |||
8 | AuthService, | 8 | AuthService, |
9 | AuthStatus, | 9 | AuthStatus, |
10 | AuthUser, | 10 | AuthUser, |
11 | MenuLink, | 11 | HooksService, |
12 | MenuSection, | ||
12 | MenuService, | 13 | MenuService, |
13 | RedirectService, | 14 | RedirectService, |
14 | ScreenService, | 15 | ScreenService, |
@@ -45,7 +46,7 @@ export class MenuComponent implements OnInit { | |||
45 | 46 | ||
46 | currentInterfaceLanguage: string | 47 | currentInterfaceLanguage: string |
47 | 48 | ||
48 | commonMenuLinks: MenuLink[] = [] | 49 | menuSections: MenuSection[] = [] |
49 | 50 | ||
50 | private languages: VideoConstant<string>[] = [] | 51 | private languages: VideoConstant<string>[] = [] |
51 | 52 | ||
@@ -71,7 +72,8 @@ export class MenuComponent implements OnInit { | |||
71 | private screenService: ScreenService, | 72 | private screenService: ScreenService, |
72 | private menuService: MenuService, | 73 | private menuService: MenuService, |
73 | private modalService: PeertubeModalService, | 74 | private modalService: PeertubeModalService, |
74 | private router: Router | 75 | private router: Router, |
76 | private hooks: HooksService | ||
75 | ) { } | 77 | ) { } |
76 | 78 | ||
77 | get isInMobileView () { | 79 | get isInMobileView () { |
@@ -88,46 +90,23 @@ export class MenuComponent implements OnInit { | |||
88 | return this.languageChooserModal.getCurrentLanguage() | 90 | return this.languageChooserModal.getCurrentLanguage() |
89 | } | 91 | } |
90 | 92 | ||
91 | get instanceName () { | ||
92 | return this.htmlServerConfig.instance.name | ||
93 | } | ||
94 | |||
95 | ngOnInit () { | 93 | ngOnInit () { |
96 | this.htmlServerConfig = this.serverService.getHTMLConfig() | 94 | this.htmlServerConfig = this.serverService.getHTMLConfig() |
97 | this.buildMenuLinks() | ||
98 | |||
99 | this.isLoggedIn = this.authService.isLoggedIn() | ||
100 | if (this.isLoggedIn === true) { | ||
101 | this.user = this.authService.getUser() | ||
102 | |||
103 | this.computeNSFWPolicy() | ||
104 | this.computeVideosLink() | ||
105 | } | ||
106 | |||
107 | this.computeAdminAccess() | ||
108 | |||
109 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() | 95 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() |
110 | 96 | ||
97 | this.updateUserState() | ||
98 | this.buildMenuSections() | ||
99 | |||
111 | this.authService.loginChangedSource.subscribe( | 100 | this.authService.loginChangedSource.subscribe( |
112 | status => { | 101 | status => { |
113 | if (status === AuthStatus.LoggedIn) { | 102 | if (status === AuthStatus.LoggedIn) { |
114 | this.isLoggedIn = true | 103 | this.isLoggedIn = true |
115 | this.user = this.authService.getUser() | ||
116 | |||
117 | this.computeAdminAccess() | ||
118 | this.computeVideosLink() | ||
119 | |||
120 | logger('Logged in.') | ||
121 | } else if (status === AuthStatus.LoggedOut) { | 104 | } else if (status === AuthStatus.LoggedOut) { |
122 | this.isLoggedIn = false | 105 | this.isLoggedIn = false |
123 | this.user = undefined | ||
124 | |||
125 | this.computeAdminAccess() | ||
126 | |||
127 | logger('Logged out.') | ||
128 | } else { | ||
129 | console.error('Unknown auth status: ' + status) | ||
130 | } | 106 | } |
107 | |||
108 | this.updateUserState() | ||
109 | this.buildMenuSections() | ||
131 | } | 110 | } |
132 | ) | 111 | ) |
133 | 112 | ||
@@ -257,8 +236,20 @@ export class MenuComponent implements OnInit { | |||
257 | } | 236 | } |
258 | } | 237 | } |
259 | 238 | ||
260 | private buildMenuLinks () { | 239 | private async buildMenuSections () { |
261 | this.commonMenuLinks = this.menuService.buildCommonLinks(this.htmlServerConfig) | 240 | const menuSections = [] |
241 | |||
242 | if (this.isLoggedIn) { | ||
243 | menuSections.push( | ||
244 | this.menuService.buildLibraryLinks(this.user?.canSeeVideosLink) | ||
245 | ) | ||
246 | } | ||
247 | |||
248 | menuSections.push( | ||
249 | this.menuService.buildCommonLinks(this.htmlServerConfig) | ||
250 | ) | ||
251 | |||
252 | this.menuSections = await this.hooks.wrapObject(menuSections, 'common', 'filter:left-menu.links.create.result') | ||
262 | } | 253 | } |
263 | 254 | ||
264 | private buildUserLanguages () { | 255 | private buildUserLanguages () { |
@@ -268,7 +259,7 @@ export class MenuComponent implements OnInit { | |||
268 | } | 259 | } |
269 | 260 | ||
270 | if (!this.user.videoLanguages) { | 261 | if (!this.user.videoLanguages) { |
271 | this.videoLanguages = [$localize`any language`] | 262 | this.videoLanguages = [ $localize`any language` ] |
272 | return | 263 | return |
273 | } | 264 | } |
274 | 265 | ||
@@ -284,6 +275,8 @@ export class MenuComponent implements OnInit { | |||
284 | } | 275 | } |
285 | 276 | ||
286 | private computeVideosLink () { | 277 | private computeVideosLink () { |
278 | if (!this.isLoggedIn) return | ||
279 | |||
287 | this.authService.userInformationLoaded | 280 | this.authService.userInformationLoaded |
288 | .pipe( | 281 | .pipe( |
289 | switchMap(() => this.user.computeCanSeeVideosLink(this.userService.getMyVideoQuotaUsed())) | 282 | switchMap(() => this.user.computeCanSeeVideosLink(this.userService.getMyVideoQuotaUsed())) |
@@ -313,4 +306,14 @@ export class MenuComponent implements OnInit { | |||
313 | break | 306 | break |
314 | } | 307 | } |
315 | } | 308 | } |
309 | |||
310 | private updateUserState () { | ||
311 | this.user = this.isLoggedIn | ||
312 | ? this.authService.getUser() | ||
313 | : undefined | ||
314 | |||
315 | this.computeAdminAccess() | ||
316 | this.computeNSFWPolicy() | ||
317 | this.computeVideosLink() | ||
318 | } | ||
316 | } | 319 | } |
diff --git a/shared/models/plugins/client/client-hook.model.ts b/shared/models/plugins/client/client-hook.model.ts index 620651051..3eedd670b 100644 --- a/shared/models/plugins/client/client-hook.model.ts +++ b/shared/models/plugins/client/client-hook.model.ts | |||
@@ -50,7 +50,10 @@ export const clientFilterHookObject = { | |||
50 | 50 | ||
51 | // Filter our SVG icons content | 51 | // Filter our SVG icons content |
52 | 'filter:internal.common.svg-icons.get-content.params': true, | 52 | 'filter:internal.common.svg-icons.get-content.params': true, |
53 | 'filter:internal.common.svg-icons.get-content.result': true | 53 | 'filter:internal.common.svg-icons.get-content.result': true, |
54 | |||
55 | // Filter left menu links | ||
56 | 'filter:left-menu.links.create.result': true | ||
54 | } | 57 | } |
55 | 58 | ||
56 | export type ClientFilterHookName = keyof typeof clientFilterHookObject | 59 | export type ClientFilterHookName = keyof typeof clientFilterHookObject |
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md index db1f1863c..f4d3c4800 100644 --- a/support/doc/plugins/guide.md +++ b/support/doc/plugins/guide.md | |||
@@ -29,6 +29,7 @@ | |||
29 | - [Add custom fields to video form](#add-custom-fields-to-video-form) | 29 | - [Add custom fields to video form](#add-custom-fields-to-video-form) |
30 | - [Register settings script](#register-settings-script) | 30 | - [Register settings script](#register-settings-script) |
31 | - [HTML placeholder elements](#html-placeholder-elements) | 31 | - [HTML placeholder elements](#html-placeholder-elements) |
32 | - [Add/remove left menu links](#addremove-left-menu-links) | ||
32 | - [Publishing](#publishing) | 33 | - [Publishing](#publishing) |
33 | - [Write a plugin/theme](#write-a-plugintheme) | 34 | - [Write a plugin/theme](#write-a-plugintheme) |
34 | - [Clone the quickstart repository](#clone-the-quickstart-repository) | 35 | - [Clone the quickstart repository](#clone-the-quickstart-repository) |
@@ -744,6 +745,11 @@ async function register (...) { | |||
744 | 745 | ||
745 | See the complete list on https://docs.joinpeertube.org/api-plugins | 746 | See the complete list on https://docs.joinpeertube.org/api-plugins |
746 | 747 | ||
748 | #### Add/remove left menu links | ||
749 | |||
750 | Left menu links can be filtered (add/remove a section or add/remove links) using the `filter:left-menu.links.create.result` client hook. | ||
751 | |||
752 | |||
747 | ### Publishing | 753 | ### Publishing |
748 | 754 | ||
749 | PeerTube plugins and themes should be published on [NPM](https://www.npmjs.com/) so that PeerTube indexes | 755 | PeerTube plugins and themes should be published on [NPM](https://www.npmjs.com/) so that PeerTube indexes |