diff --git a/client/src/app/+admin/admin-routing.module.ts b/client/src/app/+admin/admin-routing.module.ts
index 215da1e4f..79c57221b 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 98f45a7d1..9a3d90c18 100644
--- a/client/src/app/+admin/admin.component.html
+++ b/client/src/app/+admin/admin.component.html
@@ -16,6 +16,10 @@
Configuration
+
+ Plugins/Themes
+
+
System
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index 408de4837..b23999d40 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 9ab883f60..256b7e1f5 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 000000000..b75a94556
--- /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 000000000..6bb8bcd75
--- /dev/null
+++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html
@@ -0,0 +1,13 @@
+
+
+
+ {{ getNoResultMessage() }}
+
+
+
+
+ {{ plugin.name }}
+
+
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 000000000..9e98fcd34
--- /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 000000000..9745bc36b
--- /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 000000000..e69de29bb
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 000000000..5e6774739
--- /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 000000000..db1f91f3d
--- /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 000000000..e69de29bb
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 000000000..5e6774739
--- /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 000000000..f65599532
--- /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 000000000..3dc4939da
--- /dev/null
+++ b/client/src/app/+admin/plugins/plugins.component.html
@@ -0,0 +1,11 @@
+
+
+
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 000000000..9f61bcf7a
--- /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 000000000..6ec6fa4a1
--- /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 000000000..58b5534fb
--- /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 000000000..bfc2b918f
--- /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>(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 4abe9ee8d..86bde2d02 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 ad59c203b..76199d1cc 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 0876283a2..6138a32de 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)