diff --git a/client/src/app/+admin/admin-routing.module.ts b/client/src/app/+admin/admin-routing.module.ts index 215da1e4f1b9253d231c4509e10d3f2aa4a4cf20..79c57221b4fc949748a1b821d5fca72e0b188689 100644 --- a/client/src/app/+admin/admin-routing.module.ts +++ b/client/src/app/+admin/admin-routing.module.ts @@ -9,6 +9,7 @@ import { FollowsRoutes } from './follows' import { UsersRoutes } from './users' import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes' import { SystemRoutes } from '@app/+admin/system' +import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes' const adminRoutes: Routes = [ { @@ -26,7 +27,8 @@ const adminRoutes: Routes = [ ...UsersRoutes, ...ModerationRoutes, ...SystemRoutes, - ...ConfigRoutes + ...ConfigRoutes, + ...PluginsRoutes ] } ] diff --git a/client/src/app/+admin/admin.component.html b/client/src/app/+admin/admin.component.html index 98f45a7d1bf5c7f4dfb17df24aa6bb8613d7e04b..9a3d90c183911df6a0a4d968abdbe951bc9ad46f 100644 --- a/client/src/app/+admin/admin.component.html +++ b/client/src/app/+admin/admin.component.html @@ -16,6 +16,10 @@ Configuration </a> + <a i18n *ngIf="hasPluginsRight()" routerLink="/admin/plugins" routerLinkActive="active" class="title-page"> + Plugins/Themes + </a> + <a i18n *ngIf="hasJobsRight() || hasLogsRight() || hasDebugRight()" routerLink="/admin/system" routerLinkActive="active" class="title-page"> System </a> diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 408de4837bcd21a8ebf1a1324b555f8efe37a0e7..b23999d404654c8306f34c8e44f47b342c21eb64 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts @@ -28,6 +28,10 @@ export class AdminComponent { return this.auth.getUser().hasRight(UserRight.MANAGE_CONFIGURATION) } + hasPluginsRight () { + return this.auth.getUser().hasRight(UserRight.MANAGE_PLUGINS) + } + hasLogsRight () { return this.auth.getUser().hasRight(UserRight.MANAGE_LOGS) } diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 9ab883f60c4578e00775e9b37d0121ea6ead3d2f..256b7e1f5de2d86d00f77fb3833a0889f01b528f 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -21,11 +21,18 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f import { JobsComponent } from '@app/+admin/system/jobs/jobs.component' import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system' import { DebugComponent, DebugService } from '@app/+admin/system/debug' +import { PluginsComponent } from '@app/+admin/plugins/plugins.component' +import { PluginListInstalledComponent } from '@app/+admin/plugins/plugin-list-installed/plugin-list-installed.component' +import { PluginSearchComponent } from '@app/+admin/plugins/plugin-search/plugin-search.component' +import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component' +import { SelectButtonModule } from 'primeng/primeng' +import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' @NgModule({ imports: [ AdminRoutingModule, TableModule, + SelectButtonModule, SharedModule ], @@ -52,6 +59,11 @@ import { DebugComponent, DebugService } from '@app/+admin/system/debug' InstanceServerBlocklistComponent, InstanceAccountBlocklistComponent, + PluginsComponent, + PluginListInstalledComponent, + PluginSearchComponent, + PluginShowInstalledComponent, + SystemComponent, JobsComponent, LogsComponent, @@ -70,7 +82,8 @@ import { DebugComponent, DebugService } from '@app/+admin/system/debug' JobService, LogsService, DebugService, - ConfigService + ConfigService, + PluginApiService ] }) export class AdminModule { } diff --git a/client/src/app/+admin/plugins/index.ts b/client/src/app/+admin/plugins/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b75a94556017f1ca7b7b4d754974595322370fe2 --- /dev/null +++ b/client/src/app/+admin/plugins/index.ts @@ -0,0 +1 @@ +export * from './plugins.component' diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html new file mode 100644 index 0000000000000000000000000000000000000000..6bb8bcd75a6c9bc2d4c714aafd5d9376bc8d95a6 --- /dev/null +++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html @@ -0,0 +1,13 @@ +<div class="toggle-plugin-type"> + <p-selectButton [options]="pluginTypeOptions" [(ngModel)]="pluginType" (ngModelChange)="reloadPlugins()"></p-selectButton> +</div> + +<div class="no-results" i18n *ngIf="pagination.totalItems === 0"> + {{ getNoResultMessage() }} +</div> + +<div class="plugins" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"> + <div class="section plugin" *ngFor="let plugin of plugins"> + {{ plugin.name }} + </div> +</div> diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..9e98fcd340521519ea5bf3fee27856afa62abf9c --- /dev/null +++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss @@ -0,0 +1,8 @@ +@import '_variables'; +@import '_mixins'; + +.toggle-plugin-type { + display: flex; + justify-content: center; + margin-bottom: 30px; +} diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..9745bc36b8aa5ea6be964f8c9059dfe2d9ecc78d --- /dev/null +++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts @@ -0,0 +1,72 @@ +import { Component, OnInit } from '@angular/core' +import { PluginType } from '@shared/models/plugins/plugin.type' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' +import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' +import { Notifier } from '@app/core' +import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' + +@Component({ + selector: 'my-plugin-list-installed', + templateUrl: './plugin-list-installed.component.html', + styleUrls: [ './plugin-list-installed.component.scss' ] +}) +export class PluginListInstalledComponent implements OnInit { + pluginTypeOptions: { label: string, value: PluginType }[] = [] + pluginType: PluginType = PluginType.PLUGIN + + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 10 + } + sort = 'name' + + plugins: PeerTubePlugin[] = [] + + constructor ( + private i18n: I18n, + private pluginService: PluginApiService, + private notifier: Notifier + ) { + this.pluginTypeOptions = this.pluginService.getPluginTypeOptions() + } + + ngOnInit () { + this.reloadPlugins() + } + + reloadPlugins () { + this.pagination.currentPage = 1 + this.plugins = [] + + this.loadMorePlugins() + } + + loadMorePlugins () { + this.pluginService.getPlugins(this.pluginType, this.pagination, this.sort) + .subscribe( + res => { + this.plugins = this.plugins.concat(res.data) + this.pagination.totalItems = res.total + }, + + err => this.notifier.error(err.message) + ) + } + + onNearOfBottom () { + if (!hasMoreItems(this.pagination)) return + + this.pagination.currentPage += 1 + + this.loadMorePlugins() + } + + getNoResultMessage () { + if (this.pluginType === PluginType.PLUGIN) { + return this.i18n('You don\'t have plugins installed yet.') + } + + return this.i18n('You don\'t have themes installed yet.') + } +} diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..5e6774739fb643993a1068a8f5cc0ab97b73bdfe --- /dev/null +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss @@ -0,0 +1,2 @@ +@import '_variables'; +@import '_mixins'; diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..db1f91f3d03eabf2c1333daffb668c3f7b7f3d1d --- /dev/null +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit, ViewChild } from '@angular/core' +import { Notifier } from '@app/core' +import { SortMeta } from 'primeng/components/common/sortmeta' +import { ConfirmService, ServerService } from '../../../core' +import { RestPagination, RestTable, UserService } from '../../../shared' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { User } from '../../../../../../shared' +import { UserBanModalComponent } from '@app/shared/moderation' +import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' +import { PluginType } from '@shared/models/plugins/plugin.type' +import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' + +@Component({ + selector: 'my-plugin-search', + templateUrl: './plugin-search.component.html', + styleUrls: [ './plugin-search.component.scss' ] +}) +export class PluginSearchComponent implements OnInit { + pluginTypeOptions: { label: string, value: PluginType }[] = [] + + constructor ( + private i18n: I18n, + private pluginService: PluginApiService + ) { + this.pluginTypeOptions = this.pluginService.getPluginTypeOptions() + } + + ngOnInit () { + } +} diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..5e6774739fb643993a1068a8f5cc0ab97b73bdfe --- /dev/null +++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss @@ -0,0 +1,2 @@ +@import '_variables'; +@import '_mixins'; diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6559953222764c4b75241cf2b3907d40e54bb90 --- /dev/null +++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from '@angular/core' + +@Component({ + selector: 'my-plugin-show-installed', + templateUrl: './plugin-show-installed.component.html', + styleUrls: [ './plugin-show-installed.component.scss' ] +}) +export class PluginShowInstalledComponent implements OnInit { + + ngOnInit () { + + } + +} diff --git a/client/src/app/+admin/plugins/plugins.component.html b/client/src/app/+admin/plugins/plugins.component.html new file mode 100644 index 0000000000000000000000000000000000000000..3dc4939da62cad3839c7358d6bafdeaca34f199f --- /dev/null +++ b/client/src/app/+admin/plugins/plugins.component.html @@ -0,0 +1,11 @@ +<div class="admin-sub-header"> + <div i18n class="form-sub-title">Plugins/Themes</div> + + <div class="admin-sub-nav"> + <a i18n routerLink="list-installed" routerLinkActive="active">Installed</a> + + <a i18n routerLink="search" routerLinkActive="active">Search</a> + </div> +</div> + +<router-outlet></router-outlet> diff --git a/client/src/app/+admin/plugins/plugins.component.scss b/client/src/app/+admin/plugins/plugins.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..9f61bcf7a4b1cbfbe8f7441a83f4cd03b69e86ed --- /dev/null +++ b/client/src/app/+admin/plugins/plugins.component.scss @@ -0,0 +1,7 @@ +@import '_variables'; +@import '_mixins'; + +.form-sub-title { + flex-grow: 0; + margin-right: 30px; +} diff --git a/client/src/app/+admin/plugins/plugins.component.ts b/client/src/app/+admin/plugins/plugins.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ec6fa4a1ebedc352bc8672b020a9c854ce5e8b4 --- /dev/null +++ b/client/src/app/+admin/plugins/plugins.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core' + +@Component({ + templateUrl: './plugins.component.html', + styleUrls: [ './plugins.component.scss' ] +}) +export class PluginsComponent { +} diff --git a/client/src/app/+admin/plugins/plugins.routes.ts b/client/src/app/+admin/plugins/plugins.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..58b5534fb94f299fdf058785c4801276fb02594a --- /dev/null +++ b/client/src/app/+admin/plugins/plugins.routes.ts @@ -0,0 +1,53 @@ +import { Routes } from '@angular/router' + +import { UserRightGuard } from '../../core' +import { UserRight } from '../../../../../shared' +import { PluginListInstalledComponent } from '@app/+admin/plugins/plugin-list-installed/plugin-list-installed.component' +import { PluginSearchComponent } from '@app/+admin/plugins/plugin-search/plugin-search.component' +import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component' +import { PluginsComponent } from '@app/+admin/plugins/plugins.component' + +export const PluginsRoutes: Routes = [ + { + path: 'plugins', + component: PluginsComponent, + canActivate: [ UserRightGuard ], + data: { + userRight: UserRight.MANAGE_PLUGINS + }, + children: [ + { + path: '', + redirectTo: 'list-installed', + pathMatch: 'full' + }, + { + path: 'list-installed', + component: PluginListInstalledComponent, + data: { + meta: { + title: 'List installed plugins' + } + } + }, + { + path: 'search', + component: PluginSearchComponent, + data: { + meta: { + title: 'Search plugins' + } + } + }, + { + path: 'show/:name', + component: PluginShowInstalledComponent, + data: { + meta: { + title: 'Show plugin' + } + } + } + ] + } +] diff --git a/client/src/app/+admin/plugins/shared/plugin-api.service.ts b/client/src/app/+admin/plugins/shared/plugin-api.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfc2b918fc079781b97c79a70c75c3af32e0b3ed --- /dev/null +++ b/client/src/app/+admin/plugins/shared/plugin-api.service.ts @@ -0,0 +1,50 @@ +import { catchError } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { environment } from '../../../../environments/environment' +import { RestExtractor, RestService } from '../../../shared' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { PluginType } from '@shared/models/plugins/plugin.type' +import { ComponentPagination } from '@app/shared/rest/component-pagination.model' +import { ResultList } from '@shared/models' +import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' + +@Injectable() +export class PluginApiService { + private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/plugins' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private restService: RestService, + private i18n: I18n + ) { } + + getPluginTypeOptions () { + return [ + { + label: this.i18n('Plugin'), + value: PluginType.PLUGIN + }, + { + label: this.i18n('Theme'), + value: PluginType.THEME + } + ] + } + + getPlugins ( + type: PluginType, + componentPagination: ComponentPagination, + sort: string + ) { + const pagination = this.restService.componentPaginationToRestPagination(componentPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + params = params.append('type', type.toString()) + + return this.authHttp.get<ResultList<PeerTubePlugin>>(PluginApiService.BASE_APPLICATION_URL, { params }) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } +} diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index 4abe9ee8d202a59f416316044126e3858b815b32..86bde2d02369bc3bdef7542147d6debd0d614c89 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts @@ -5,7 +5,7 @@ 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.model' +import { RegisterHookOptions } from '@shared/models/plugins/register-hook.model' import { ReplaySubject } from 'rxjs' import { first, shareReplay } from 'rxjs/operators' diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts index ad59c203be07d79abe8a826a4407ee9d9be93777..76199d1cc03215a96cdc4261932f287e69716587 100644 --- a/client/src/app/core/theme/theme.service.ts +++ b/client/src/app/core/theme/theme.service.ts @@ -83,6 +83,7 @@ export class ThemeService { console.log('Enabling %s theme.', currentTheme) this.loadTheme(currentTheme) + const theme = this.getTheme(currentTheme) if (theme) { console.log('Adding scripts of theme %s.', currentTheme) @@ -95,6 +96,10 @@ export class ThemeService { } private listenUserTheme () { + if (!this.auth.isLoggedIn()) { + this.updateCurrentTheme() + } + this.auth.userInformationLoaded .subscribe(() => this.updateCurrentTheme()) } diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 0876283a2022436aff4fa95ec0db666ac2a9cf11..6138a32de9305c0d1877ab3042a42d0377e80bcb 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts @@ -14,7 +14,7 @@ import { searchRouter } from './search' import { overviewsRouter } from './overviews' import { videoPlaylistRouter } from './video-playlist' import { CONFIG } from '../../initializers/config' -import { pluginsRouter } from '../plugins' +import { pluginRouter } from './plugins' const apiRouter = express.Router() @@ -43,7 +43,7 @@ apiRouter.use('/videos', videosRouter) apiRouter.use('/jobs', jobsRouter) apiRouter.use('/search', searchRouter) apiRouter.use('/overviews', overviewsRouter) -apiRouter.use('/plugins', pluginsRouter) +apiRouter.use('/plugins', pluginRouter) apiRouter.use('/ping', pong) apiRouter.use('/*', badRequest)