diff options
27 files changed, 512 insertions, 80 deletions
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 index f10b4eb8d..6d2155332 100644 --- 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 | |||
@@ -2,7 +2,7 @@ | |||
2 | <p-selectButton [options]="pluginTypeOptions" [(ngModel)]="pluginType" (ngModelChange)="reloadPlugins()"></p-selectButton> | 2 | <p-selectButton [options]="pluginTypeOptions" [(ngModel)]="pluginType" (ngModelChange)="reloadPlugins()"></p-selectButton> |
3 | </div> | 3 | </div> |
4 | 4 | ||
5 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0"> | 5 | <div class="no-results" *ngIf="pagination.totalItems === 0"> |
6 | {{ getNoResultMessage() }} | 6 | {{ getNoResultMessage() }} |
7 | </div> | 7 | </div> |
8 | 8 | ||
@@ -28,7 +28,7 @@ | |||
28 | 28 | ||
29 | <my-edit-button *ngIf="pluginType !== PluginType.THEME" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button> | 29 | <my-edit-button *ngIf="pluginType !== PluginType.THEME" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button> |
30 | 30 | ||
31 | <my-button class="update-button" *ngIf="!isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)" | 31 | <my-button class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)" |
32 | [label]="getUpdateLabel(plugin)" icon="refresh" [attr.disabled]="isUpdating(plugin)" | 32 | [label]="getUpdateLabel(plugin)" icon="refresh" [attr.disabled]="isUpdating(plugin)" |
33 | ></my-button> | 33 | ></my-button> |
34 | 34 | ||
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 index 7641c507b..0b54ffda3 100644 --- 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 | |||
@@ -1,41 +1,6 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .first-row { | ||
5 | margin-bottom: 10px; | ||
6 | |||
7 | .plugin-name { | ||
8 | font-size: 16px; | ||
9 | margin-right: 10px; | ||
10 | font-weight: $font-semibold; | ||
11 | } | ||
12 | |||
13 | .plugin-version { | ||
14 | opacity: 0.6; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .second-row { | ||
19 | display: flex; | ||
20 | align-items: center; | ||
21 | justify-content: space-between; | ||
22 | |||
23 | .description { | ||
24 | opacity: 0.8 | ||
25 | } | ||
26 | |||
27 | .buttons { | ||
28 | > *:not(:last-child) { | ||
29 | margin-right: 10px; | ||
30 | } | ||
31 | } | ||
32 | } | ||
33 | |||
34 | .action-button { | ||
35 | @include peertube-button-link; | ||
36 | @include button-with-icon(21px, 0, -2px); | ||
37 | } | ||
38 | |||
39 | .update-button[disabled="true"] /deep/ .action-button { | 4 | .update-button[disabled="true"] /deep/ .action-button { |
40 | cursor: default !important; | 5 | cursor: default !important; |
41 | } | 6 | } |
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 index 67a11c3a8..9809759db 100644 --- 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 | |||
@@ -6,13 +6,14 @@ import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pa | |||
6 | import { ConfirmService, Notifier } from '@app/core' | 6 | import { ConfirmService, Notifier } from '@app/core' |
7 | import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' | 7 | import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' |
8 | import { ActivatedRoute, Router } from '@angular/router' | 8 | import { ActivatedRoute, Router } from '@angular/router' |
9 | import { compareSemVer } from '@app/shared/misc/utils' | 9 | import { compareSemVer } from '@shared/core-utils/miscs/miscs' |
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
12 | selector: 'my-plugin-list-installed', | 12 | selector: 'my-plugin-list-installed', |
13 | templateUrl: './plugin-list-installed.component.html', | 13 | templateUrl: './plugin-list-installed.component.html', |
14 | styleUrls: [ | 14 | styleUrls: [ |
15 | '../shared/toggle-plugin-type.scss', | 15 | '../shared/toggle-plugin-type.scss', |
16 | '../shared/plugin-list.component.scss', | ||
16 | './plugin-list-installed.component.scss' | 17 | './plugin-list-installed.component.scss' |
17 | ] | 18 | ] |
18 | }) | 19 | }) |
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 index e69de29bb..7dd103979 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html | |||
@@ -0,0 +1,55 @@ | |||
1 | <div class="toggle-plugin-type"> | ||
2 | <p-selectButton [options]="pluginTypeOptions" [(ngModel)]="pluginType" (ngModelChange)="reloadPlugins()"></p-selectButton> | ||
3 | </div> | ||
4 | |||
5 | <div class="search-bar"> | ||
6 | <input type="text" (input)="onSearchChange($event.target.value)" i18n-placeholder placeholder="Search..."/> | ||
7 | </div> | ||
8 | |||
9 | <div class="result-title" *ngIf="!isSearching"> | ||
10 | <ng-container *ngIf="!search"> | ||
11 | <my-global-icon iconName="trending"></my-global-icon> | ||
12 | <ng-container i18n>Popular</ng-container> | ||
13 | </ng-container> | ||
14 | |||
15 | <ng-container i18n *ngIf="!!search"> | ||
16 | <my-global-icon iconName="search"></my-global-icon> | ||
17 | |||
18 | <ng-container i18n> | ||
19 | {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}" | ||
20 | </ng-container> | ||
21 | </ng-container> | ||
22 | </div> | ||
23 | |||
24 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0"> | ||
25 | No results. | ||
26 | </div> | ||
27 | |||
28 | <div class="plugins" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"> | ||
29 | <div class="card plugin" *ngFor="let plugin of plugins"> | ||
30 | <div class="card-body"> | ||
31 | <div class="first-row"> | ||
32 | <span class="plugin-name">{{ plugin.name }}</span> | ||
33 | |||
34 | <span class="plugin-version">{{ plugin.latestVersion }}</span> | ||
35 | </div> | ||
36 | |||
37 | <div class="second-row"> | ||
38 | <div class="description">{{ plugin.description }}</div> | ||
39 | |||
40 | <div class="buttons"> | ||
41 | <a class="action-button action-button-edit grey-button" target="_blank" rel="noopener noreferrer" | ||
42 | [href]="plugin.homepage" i18n-title title="Go to the plugin homepage" | ||
43 | > | ||
44 | <my-global-icon iconName="go"></my-global-icon> | ||
45 | <span i18n class="button-label">Homepage</span> | ||
46 | </a> | ||
47 | |||
48 | <my-button class="update-button" *ngIf="plugin.installed === false" (click)="install(plugin)" [loading]="isInstalling(plugin)" | ||
49 | label="Install" icon="cloud-download" [attr.disabled]="isInstalling(plugin)" | ||
50 | ></my-button> | ||
51 | </div> | ||
52 | </div> | ||
53 | </div> | ||
54 | </div> | ||
55 | </div> | ||
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 index 5e6774739..ad6ff89da 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss | |||
@@ -1,2 +1,23 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | |||
4 | .search-bar { | ||
5 | display: flex; | ||
6 | justify-content: center; | ||
7 | margin: 30px 0; | ||
8 | |||
9 | input { | ||
10 | @include peertube-input-text(60%); | ||
11 | height: 35px; | ||
12 | } | ||
13 | } | ||
14 | |||
15 | .result-title { | ||
16 | font-size: 22px; | ||
17 | font-weight: 600; | ||
18 | margin-bottom: 15px; | ||
19 | |||
20 | my-global-icon { | ||
21 | margin-right: 5px; | ||
22 | } | ||
23 | } | ||
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 index 787be2c8c..935e11362 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts | |||
@@ -1,33 +1,133 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { ConfirmService } from '../../../core' |
4 | import { ConfirmService, ServerService } from '../../../core' | ||
5 | import { RestPagination, RestTable, UserService } from '../../../shared' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { User } from '../../../../../../shared' | ||
8 | import { UserBanModalComponent } from '@app/shared/moderation' | ||
9 | import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' | ||
10 | import { PluginType } from '@shared/models/plugins/plugin.type' | 5 | import { PluginType } from '@shared/models/plugins/plugin.type' |
11 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' | 6 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' |
7 | import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' | ||
8 | import { ActivatedRoute, Router } from '@angular/router' | ||
9 | import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' | ||
10 | import { Subject } from 'rxjs' | ||
11 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | ||
12 | 12 | ||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-plugin-search', | 14 | selector: 'my-plugin-search', |
15 | templateUrl: './plugin-search.component.html', | 15 | templateUrl: './plugin-search.component.html', |
16 | styleUrls: [ | 16 | styleUrls: [ |
17 | '../shared/toggle-plugin-type.scss', | 17 | '../shared/toggle-plugin-type.scss', |
18 | '../shared/plugin-list.component.scss', | ||
18 | './plugin-search.component.scss' | 19 | './plugin-search.component.scss' |
19 | ] | 20 | ] |
20 | }) | 21 | }) |
21 | export class PluginSearchComponent implements OnInit { | 22 | export class PluginSearchComponent implements OnInit { |
22 | pluginTypeOptions: { label: string, value: PluginType }[] = [] | 23 | pluginTypeOptions: { label: string, value: PluginType }[] = [] |
24 | pluginType: PluginType = PluginType.PLUGIN | ||
25 | |||
26 | pagination: ComponentPagination = { | ||
27 | currentPage: 1, | ||
28 | itemsPerPage: 10 | ||
29 | } | ||
30 | sort = '-popularity' | ||
31 | |||
32 | search = '' | ||
33 | isSearching = false | ||
34 | |||
35 | plugins: PeerTubePluginIndex[] = [] | ||
36 | installing: { [name: string]: boolean } = {} | ||
37 | |||
38 | private searchSubject = new Subject<string>() | ||
23 | 39 | ||
24 | constructor ( | 40 | constructor ( |
25 | private i18n: I18n, | 41 | private i18n: I18n, |
26 | private pluginService: PluginApiService | 42 | private pluginService: PluginApiService, |
43 | private notifier: Notifier, | ||
44 | private confirmService: ConfirmService, | ||
45 | private router: Router, | ||
46 | private route: ActivatedRoute | ||
27 | ) { | 47 | ) { |
28 | this.pluginTypeOptions = this.pluginService.getPluginTypeOptions() | 48 | this.pluginTypeOptions = this.pluginService.getPluginTypeOptions() |
29 | } | 49 | } |
30 | 50 | ||
31 | ngOnInit () { | 51 | ngOnInit () { |
52 | const query = this.route.snapshot.queryParams | ||
53 | if (query['pluginType']) this.pluginType = parseInt(query['pluginType'], 10) | ||
54 | |||
55 | this.searchSubject.asObservable() | ||
56 | .pipe( | ||
57 | debounceTime(400), | ||
58 | distinctUntilChanged() | ||
59 | ) | ||
60 | .subscribe(search => { | ||
61 | this.search = search | ||
62 | this.reloadPlugins() | ||
63 | }) | ||
64 | |||
65 | this.reloadPlugins() | ||
66 | } | ||
67 | |||
68 | onSearchChange (search: string) { | ||
69 | this.searchSubject.next(search) | ||
70 | } | ||
71 | |||
72 | reloadPlugins () { | ||
73 | this.pagination.currentPage = 1 | ||
74 | this.plugins = [] | ||
75 | |||
76 | this.router.navigate([], { queryParams: { pluginType: this.pluginType } }) | ||
77 | |||
78 | this.loadMorePlugins() | ||
79 | } | ||
80 | |||
81 | loadMorePlugins () { | ||
82 | this.isSearching = true | ||
83 | |||
84 | this.pluginService.searchAvailablePlugins(this.pluginType, this.pagination, this.sort, this.search) | ||
85 | .subscribe( | ||
86 | res => { | ||
87 | this.isSearching = false | ||
88 | |||
89 | this.plugins = this.plugins.concat(res.data) | ||
90 | this.pagination.totalItems = res.total | ||
91 | }, | ||
92 | |||
93 | err => this.notifier.error(err.message) | ||
94 | ) | ||
95 | } | ||
96 | |||
97 | onNearOfBottom () { | ||
98 | if (!hasMoreItems(this.pagination)) return | ||
99 | |||
100 | this.pagination.currentPage += 1 | ||
101 | |||
102 | this.loadMorePlugins() | ||
103 | } | ||
104 | |||
105 | isInstalling (plugin: PeerTubePluginIndex) { | ||
106 | return !!this.installing[plugin.npmName] | ||
107 | } | ||
108 | |||
109 | async install (plugin: PeerTubePluginIndex) { | ||
110 | if (this.installing[plugin.npmName]) return | ||
111 | |||
112 | const res = await this.confirmService.confirm( | ||
113 | this.i18n('Please only install plugins or themes you trust, since they can execute any code on your instance.'), | ||
114 | this.i18n('Install {{pluginName}}?', { pluginName: plugin.name }) | ||
115 | ) | ||
116 | if (res === false) return | ||
117 | |||
118 | this.installing[plugin.npmName] = true | ||
119 | |||
120 | this.pluginService.install(plugin.npmName) | ||
121 | .subscribe( | ||
122 | () => { | ||
123 | this.installing[plugin.npmName] = false | ||
124 | |||
125 | this.notifier.success(this.i18n('{{pluginName}} installed.', { pluginName: plugin.name })) | ||
126 | |||
127 | plugin.installed = true | ||
128 | }, | ||
129 | |||
130 | err => this.notifier.error(err.message) | ||
131 | ) | ||
32 | } | 132 | } |
33 | } | 133 | } |
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 index 8750bfd38..b99281a37 100644 --- 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 | |||
@@ -7,7 +7,7 @@ import { ActivatedRoute } from '@angular/router' | |||
7 | import { Subscription } from 'rxjs' | 7 | import { Subscription } from 'rxjs' |
8 | import { map, switchMap } from 'rxjs/operators' | 8 | import { map, switchMap } from 'rxjs/operators' |
9 | import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model' | 9 | import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model' |
10 | import { BuildFormArgument, BuildFormDefaultValues, FormReactive, FormValidatorService } from '@app/shared' | 10 | import { BuildFormArgument, FormReactive, FormValidatorService } from '@app/shared' |
11 | 11 | ||
12 | @Component({ | 12 | @Component({ |
13 | selector: 'my-plugin-show-installed', | 13 | selector: 'my-plugin-show-installed', |
@@ -83,7 +83,6 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit | |||
83 | } | 83 | } |
84 | 84 | ||
85 | private buildSettingsForm () { | 85 | private buildSettingsForm () { |
86 | const defaultValues: BuildFormDefaultValues = {} | ||
87 | const buildOptions: BuildFormArgument = {} | 86 | const buildOptions: BuildFormArgument = {} |
88 | const settingsValues: any = {} | 87 | const settingsValues: any = {} |
89 | 88 | ||
diff --git a/client/src/app/+admin/plugins/shared/plugin-api.service.ts b/client/src/app/+admin/plugins/shared/plugin-api.service.ts index 89f190675..51f086a93 100644 --- a/client/src/app/+admin/plugins/shared/plugin-api.service.ts +++ b/client/src/app/+admin/plugins/shared/plugin-api.service.ts | |||
@@ -11,6 +11,7 @@ import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' | |||
11 | import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model' | 11 | import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model' |
12 | import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model' | 12 | import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model' |
13 | import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model' | 13 | import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model' |
14 | import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' | ||
14 | 15 | ||
15 | @Injectable() | 16 | @Injectable() |
16 | export class PluginApiService { | 17 | export class PluginApiService { |
@@ -45,7 +46,7 @@ export class PluginApiService { | |||
45 | } | 46 | } |
46 | 47 | ||
47 | getPlugins ( | 48 | getPlugins ( |
48 | type: PluginType, | 49 | pluginType: PluginType, |
49 | componentPagination: ComponentPagination, | 50 | componentPagination: ComponentPagination, |
50 | sort: string | 51 | sort: string |
51 | ) { | 52 | ) { |
@@ -53,12 +54,30 @@ export class PluginApiService { | |||
53 | 54 | ||
54 | let params = new HttpParams() | 55 | let params = new HttpParams() |
55 | params = this.restService.addRestGetParams(params, pagination, sort) | 56 | params = this.restService.addRestGetParams(params, pagination, sort) |
56 | params = params.append('type', type.toString()) | 57 | params = params.append('pluginType', pluginType.toString()) |
57 | 58 | ||
58 | return this.authHttp.get<ResultList<PeerTubePlugin>>(PluginApiService.BASE_APPLICATION_URL, { params }) | 59 | return this.authHttp.get<ResultList<PeerTubePlugin>>(PluginApiService.BASE_APPLICATION_URL, { params }) |
59 | .pipe(catchError(res => this.restExtractor.handleError(res))) | 60 | .pipe(catchError(res => this.restExtractor.handleError(res))) |
60 | } | 61 | } |
61 | 62 | ||
63 | searchAvailablePlugins ( | ||
64 | pluginType: PluginType, | ||
65 | componentPagination: ComponentPagination, | ||
66 | sort: string, | ||
67 | search?: string | ||
68 | ) { | ||
69 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
70 | |||
71 | let params = new HttpParams() | ||
72 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
73 | params = params.append('pluginType', pluginType.toString()) | ||
74 | |||
75 | if (search) params = params.append('search', search) | ||
76 | |||
77 | return this.authHttp.get<ResultList<PeerTubePluginIndex>>(PluginApiService.BASE_APPLICATION_URL + '/available', { params }) | ||
78 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
79 | } | ||
80 | |||
62 | getPlugin (npmName: string) { | 81 | getPlugin (npmName: string) { |
63 | const path = PluginApiService.BASE_APPLICATION_URL + '/' + npmName | 82 | const path = PluginApiService.BASE_APPLICATION_URL + '/' + npmName |
64 | 83 | ||
diff --git a/client/src/app/+admin/plugins/shared/plugin-list.component.scss b/client/src/app/+admin/plugins/shared/plugin-list.component.scss new file mode 100644 index 000000000..f250404ed --- /dev/null +++ b/client/src/app/+admin/plugins/shared/plugin-list.component.scss | |||
@@ -0,0 +1,37 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .first-row { | ||
5 | margin-bottom: 10px; | ||
6 | |||
7 | .plugin-name { | ||
8 | font-size: 16px; | ||
9 | margin-right: 10px; | ||
10 | font-weight: $font-semibold; | ||
11 | } | ||
12 | |||
13 | .plugin-version { | ||
14 | opacity: 0.6; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .second-row { | ||
19 | display: flex; | ||
20 | align-items: center; | ||
21 | justify-content: space-between; | ||
22 | |||
23 | .description { | ||
24 | opacity: 0.8 | ||
25 | } | ||
26 | |||
27 | .buttons { | ||
28 | > *:not(:last-child) { | ||
29 | margin-right: 10px; | ||
30 | } | ||
31 | } | ||
32 | } | ||
33 | |||
34 | .action-button { | ||
35 | @include peertube-button-link; | ||
36 | @include button-with-icon(21px, 0, -2px); | ||
37 | } | ||
diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts index 5a3db4531..5b525dec1 100644 --- a/client/src/app/shared/images/global-icon.component.ts +++ b/client/src/app/shared/images/global-icon.component.ts | |||
@@ -45,6 +45,7 @@ const icons = { | |||
45 | 'administration': require('../../../assets/images/menu/administration.html'), | 45 | 'administration': require('../../../assets/images/menu/administration.html'), |
46 | 'subscriptions': require('../../../assets/images/menu/subscriptions.html'), | 46 | 'subscriptions': require('../../../assets/images/menu/subscriptions.html'), |
47 | 'users': require('../../../assets/images/global/users.html'), | 47 | 'users': require('../../../assets/images/global/users.html'), |
48 | 'search': require('../../../assets/images/global/search.html'), | ||
48 | 'refresh': require('../../../assets/images/global/refresh.html') | 49 | 'refresh': require('../../../assets/images/global/refresh.html') |
49 | } | 50 | } |
50 | 51 | ||
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts index 098496d45..85fc1c3a0 100644 --- a/client/src/app/shared/misc/utils.ts +++ b/client/src/app/shared/misc/utils.ts | |||
@@ -134,23 +134,6 @@ function scrollToTop () { | |||
134 | window.scroll(0, 0) | 134 | window.scroll(0, 0) |
135 | } | 135 | } |
136 | 136 | ||
137 | // Thanks https://stackoverflow.com/a/16187766 | ||
138 | function compareSemVer (a: string, b: string) { | ||
139 | const regExStrip0 = /(\.0+)+$/ | ||
140 | const segmentsA = a.replace(regExStrip0, '').split('.') | ||
141 | const segmentsB = b.replace(regExStrip0, '').split('.') | ||
142 | |||
143 | const l = Math.min(segmentsA.length, segmentsB.length) | ||
144 | |||
145 | for (let i = 0; i < l; i++) { | ||
146 | const diff = parseInt(segmentsA[ i ], 10) - parseInt(segmentsB[ i ], 10) | ||
147 | |||
148 | if (diff) return diff | ||
149 | } | ||
150 | |||
151 | return segmentsA.length - segmentsB.length | ||
152 | } | ||
153 | |||
154 | export { | 137 | export { |
155 | sortBy, | 138 | sortBy, |
156 | durationToString, | 139 | durationToString, |
@@ -161,7 +144,6 @@ export { | |||
161 | getAbsoluteAPIUrl, | 144 | getAbsoluteAPIUrl, |
162 | dateToHuman, | 145 | dateToHuman, |
163 | immutableAssign, | 146 | immutableAssign, |
164 | compareSemVer, | ||
165 | objectToFormData, | 147 | objectToFormData, |
166 | objectLineFeedToHtml, | 148 | objectLineFeedToHtml, |
167 | removeElementFromArray, | 149 | removeElementFromArray, |
diff --git a/client/src/assets/images/global/search.html b/client/src/assets/images/global/search.html new file mode 100644 index 000000000..46ac5848b --- /dev/null +++ b/client/src/assets/images/global/search.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <defs></defs> | ||
3 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
4 | <g id="Artboard-4" transform="translate(-136.000000, -115.000000)" stroke="#000" stroke-width="2"> | ||
5 | <g id="3" transform="translate(136.000000, 115.000000)"> | ||
6 | <circle id="Oval-3" cx="10" cy="10" r="7"></circle> | ||
7 | <path d="M15,15 L21,21" id="Path-3" stroke-linecap="round" stroke-linejoin="round"></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/config/default.yaml b/config/default.yaml index a1b2991cf..341de49b8 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -155,6 +155,13 @@ views: | |||
155 | remote: | 155 | remote: |
156 | max_age: -1 | 156 | max_age: -1 |
157 | 157 | ||
158 | plugins: | ||
159 | # The website PeerTube will ask for available PeerTube plugins | ||
160 | # This is an unmoderated plugin index, so only install plugins you trust | ||
161 | index: | ||
162 | enabled: true | ||
163 | url: 'https://packages.joinpeertube.org' | ||
164 | |||
158 | cache: | 165 | cache: |
159 | previews: | 166 | previews: |
160 | size: 500 # Max number of previews you want to cache | 167 | size: 500 # Max number of previews you want to cache |
diff --git a/config/production.yaml.example b/config/production.yaml.example index 6c2eb4416..28f078efe 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -156,6 +156,13 @@ views: | |||
156 | remote: | 156 | remote: |
157 | max_age: -1 | 157 | max_age: -1 |
158 | 158 | ||
159 | plugins: | ||
160 | # The website PeerTube will ask for available PeerTube plugins | ||
161 | # This is an unmoderated plugin index, so only install plugins you trust | ||
162 | index: | ||
163 | enabled: true | ||
164 | url: 'https://packages.joinpeertube.org' | ||
165 | |||
159 | 166 | ||
160 | ############################################################################### | 167 | ############################################################################### |
161 | # | 168 | # |
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts index 14675fdf3..114cc49b6 100644 --- a/server/controllers/api/plugins.ts +++ b/server/controllers/api/plugins.ts | |||
@@ -8,12 +8,13 @@ import { | |||
8 | setDefaultPagination, | 8 | setDefaultPagination, |
9 | setDefaultSort | 9 | setDefaultSort |
10 | } from '../../middlewares' | 10 | } from '../../middlewares' |
11 | import { pluginsSortValidator } from '../../middlewares/validators' | 11 | import { availablePluginsSortValidator, pluginsSortValidator } from '../../middlewares/validators' |
12 | import { PluginModel } from '../../models/server/plugin' | 12 | import { PluginModel } from '../../models/server/plugin' |
13 | import { UserRight } from '../../../shared/models/users' | 13 | import { UserRight } from '../../../shared/models/users' |
14 | import { | 14 | import { |
15 | existingPluginValidator, | 15 | existingPluginValidator, |
16 | installOrUpdatePluginValidator, | 16 | installOrUpdatePluginValidator, |
17 | listAvailablePluginsValidator, | ||
17 | listPluginsValidator, | 18 | listPluginsValidator, |
18 | uninstallPluginValidator, | 19 | uninstallPluginValidator, |
19 | updatePluginSettingsValidator | 20 | updatePluginSettingsValidator |
@@ -22,9 +23,22 @@ import { PluginManager } from '../../lib/plugins/plugin-manager' | |||
22 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' | 23 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' |
23 | import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model' | 24 | import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model' |
24 | import { logger } from '../../helpers/logger' | 25 | import { logger } from '../../helpers/logger' |
26 | import { listAvailablePluginsFromIndex } from '../../lib/plugins/plugin-index' | ||
27 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | ||
25 | 28 | ||
26 | const pluginRouter = express.Router() | 29 | const pluginRouter = express.Router() |
27 | 30 | ||
31 | pluginRouter.get('/available', | ||
32 | authenticate, | ||
33 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | ||
34 | listAvailablePluginsValidator, | ||
35 | paginationValidator, | ||
36 | availablePluginsSortValidator, | ||
37 | setDefaultSort, | ||
38 | setDefaultPagination, | ||
39 | asyncMiddleware(listAvailablePlugins) | ||
40 | ) | ||
41 | |||
28 | pluginRouter.get('/', | 42 | pluginRouter.get('/', |
29 | authenticate, | 43 | authenticate, |
30 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | 44 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), |
@@ -88,10 +102,10 @@ export { | |||
88 | // --------------------------------------------------------------------------- | 102 | // --------------------------------------------------------------------------- |
89 | 103 | ||
90 | async function listPlugins (req: express.Request, res: express.Response) { | 104 | async function listPlugins (req: express.Request, res: express.Response) { |
91 | const type = req.query.type | 105 | const pluginType = req.query.pluginType |
92 | 106 | ||
93 | const resultList = await PluginModel.listForApi({ | 107 | const resultList = await PluginModel.listForApi({ |
94 | type, | 108 | pluginType, |
95 | start: req.query.start, | 109 | start: req.query.start, |
96 | count: req.query.count, | 110 | count: req.query.count, |
97 | sort: req.query.sort | 111 | sort: req.query.sort |
@@ -160,3 +174,11 @@ async function updatePluginSettings (req: express.Request, res: express.Response | |||
160 | 174 | ||
161 | return res.sendStatus(204) | 175 | return res.sendStatus(204) |
162 | } | 176 | } |
177 | |||
178 | async function listAvailablePlugins (req: express.Request, res: express.Response) { | ||
179 | const query: PeertubePluginIndexList = req.query | ||
180 | |||
181 | const resultList = await listAvailablePluginsFromIndex(query) | ||
182 | |||
183 | return res.json(resultList) | ||
184 | } | ||
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index dfc4bea21..2c1b30021 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -134,6 +134,12 @@ const CONFIG = { | |||
134 | } | 134 | } |
135 | } | 135 | } |
136 | }, | 136 | }, |
137 | PLUGINS: { | ||
138 | INDEX: { | ||
139 | ENABLED: config.get<boolean>('plugins.index.enabled'), | ||
140 | URL: config.get<boolean>('plugins.index.url') | ||
141 | } | ||
142 | }, | ||
137 | ADMIN: { | 143 | ADMIN: { |
138 | get EMAIL () { return config.get<string>('admin.email') } | 144 | get EMAIL () { return config.get<string>('admin.email') } |
139 | }, | 145 | }, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 2d487a263..06e8c070b 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -64,7 +64,9 @@ const SORTABLE_COLUMNS = { | |||
64 | 64 | ||
65 | VIDEO_PLAYLISTS: [ 'displayName', 'createdAt', 'updatedAt' ], | 65 | VIDEO_PLAYLISTS: [ 'displayName', 'createdAt', 'updatedAt' ], |
66 | 66 | ||
67 | PLUGINS: [ 'name', 'createdAt', 'updatedAt' ] | 67 | PLUGINS: [ 'name', 'createdAt', 'updatedAt' ], |
68 | |||
69 | AVAILABLE_PLUGINS: [ 'npmName', 'popularity' ] | ||
68 | } | 70 | } |
69 | 71 | ||
70 | const OAUTH_LIFETIME = { | 72 | const OAUTH_LIFETIME = { |
@@ -165,6 +167,7 @@ const SCHEDULER_INTERVALS_MS = { | |||
165 | removeOldJobs: 60000 * 60, // 1 hour | 167 | removeOldJobs: 60000 * 60, // 1 hour |
166 | updateVideos: 60000, // 1 minute | 168 | updateVideos: 60000, // 1 minute |
167 | youtubeDLUpdate: 60000 * 60 * 24, // 1 day | 169 | youtubeDLUpdate: 60000 * 60 * 24, // 1 day |
170 | checkPlugins: 60000 * 60 * 24, // 1 day | ||
168 | removeOldViews: 60000 * 60 * 24, // 1 day | 171 | removeOldViews: 60000 * 60 * 24, // 1 day |
169 | removeOldHistory: 60000 * 60 * 24 // 1 day | 172 | removeOldHistory: 60000 * 60 * 24 // 1 day |
170 | } | 173 | } |
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts new file mode 100644 index 000000000..4a8a90ec8 --- /dev/null +++ b/server/lib/plugins/plugin-index.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | import { doRequest } from '../../helpers/requests' | ||
2 | import { CONFIG } from '../../initializers/config' | ||
3 | import { | ||
4 | PeertubePluginLatestVersionRequest, | ||
5 | PeertubePluginLatestVersionResponse | ||
6 | } from '../../../shared/models/plugins/peertube-plugin-latest-version.model' | ||
7 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | ||
8 | import { ResultList } from '../../../shared/models' | ||
9 | import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' | ||
10 | import { PluginModel } from '../../models/server/plugin' | ||
11 | import { PluginManager } from './plugin-manager' | ||
12 | import { logger } from '../../helpers/logger' | ||
13 | |||
14 | const packageJSON = require('../../../../package.json') | ||
15 | |||
16 | async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { | ||
17 | const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options | ||
18 | |||
19 | const qs: PeertubePluginIndexList = { | ||
20 | start, | ||
21 | count, | ||
22 | sort, | ||
23 | pluginType, | ||
24 | search, | ||
25 | currentPeerTubeEngine: packageJSON.version | ||
26 | } | ||
27 | |||
28 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' | ||
29 | |||
30 | const { body } = await doRequest({ uri, qs, json: true }) | ||
31 | |||
32 | logger.debug('Got result from PeerTube index.', { body }) | ||
33 | |||
34 | await addInstanceInformation(body) | ||
35 | |||
36 | return body as ResultList<PeerTubePluginIndex> | ||
37 | } | ||
38 | |||
39 | async function addInstanceInformation (result: ResultList<PeerTubePluginIndex>) { | ||
40 | for (const d of result.data) { | ||
41 | d.installed = PluginManager.Instance.isRegistered(d.npmName) | ||
42 | d.name = PluginModel.normalizePluginName(d.npmName) | ||
43 | } | ||
44 | |||
45 | return result | ||
46 | } | ||
47 | |||
48 | async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePluginLatestVersionResponse> { | ||
49 | const bodyRequest: PeertubePluginLatestVersionRequest = { | ||
50 | npmNames, | ||
51 | currentPeerTubeEngine: packageJSON.version | ||
52 | } | ||
53 | |||
54 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version' | ||
55 | |||
56 | const { body } = await doRequest({ uri, body: bodyRequest }) | ||
57 | |||
58 | return body | ||
59 | } | ||
60 | |||
61 | export { | ||
62 | listAvailablePluginsFromIndex, | ||
63 | getLatestPluginsVersion | ||
64 | } | ||
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 7576b284c..9e4ec5adf 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -55,6 +55,10 @@ export class PluginManager { | |||
55 | 55 | ||
56 | // ###################### Getters ###################### | 56 | // ###################### Getters ###################### |
57 | 57 | ||
58 | isRegistered (npmName: string) { | ||
59 | return !!this.getRegisteredPluginOrTheme(npmName) | ||
60 | } | ||
61 | |||
58 | getRegisteredPluginOrTheme (npmName: string) { | 62 | getRegisteredPluginOrTheme (npmName: string) { |
59 | return this.registeredPlugins[npmName] | 63 | return this.registeredPlugins[npmName] |
60 | } | 64 | } |
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts new file mode 100644 index 000000000..9c60dbcd4 --- /dev/null +++ b/server/lib/schedulers/plugins-check-scheduler.ts | |||
@@ -0,0 +1,60 @@ | |||
1 | import { logger } from '../../helpers/logger' | ||
2 | import { AbstractScheduler } from './abstract-scheduler' | ||
3 | import { retryTransactionWrapper } from '../../helpers/database-utils' | ||
4 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | ||
5 | import { CONFIG } from '../../initializers/config' | ||
6 | import { PluginModel } from '../../models/server/plugin' | ||
7 | import { chunk } from 'lodash' | ||
8 | import { getLatestPluginsVersion } from '../plugins/plugin-index' | ||
9 | import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' | ||
10 | |||
11 | export class PluginsCheckScheduler extends AbstractScheduler { | ||
12 | |||
13 | private static instance: AbstractScheduler | ||
14 | |||
15 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPlugins | ||
16 | |||
17 | private constructor () { | ||
18 | super() | ||
19 | } | ||
20 | |||
21 | protected async internalExecute () { | ||
22 | return retryTransactionWrapper(this.checkLatestPluginsVersion.bind(this)) | ||
23 | } | ||
24 | |||
25 | private async checkLatestPluginsVersion () { | ||
26 | if (CONFIG.PLUGINS.INDEX.ENABLED === false) return | ||
27 | |||
28 | logger.info('Checkin latest plugins version.') | ||
29 | |||
30 | const plugins = await PluginModel.listInstalled() | ||
31 | |||
32 | // Process 10 plugins in 1 HTTP request | ||
33 | const chunks = chunk(plugins, 10) | ||
34 | for (const chunk of chunks) { | ||
35 | // Find plugins according to their npm name | ||
36 | const pluginIndex: { [npmName: string]: PluginModel} = {} | ||
37 | for (const plugin of chunk) { | ||
38 | pluginIndex[PluginModel.buildNpmName(plugin.name, plugin.type)] = plugin | ||
39 | } | ||
40 | |||
41 | const npmNames = Object.keys(pluginIndex) | ||
42 | const results = await getLatestPluginsVersion(npmNames) | ||
43 | |||
44 | for (const result of results) { | ||
45 | const plugin = pluginIndex[result.npmName] | ||
46 | if (!result.latestVersion) continue | ||
47 | |||
48 | if (plugin.latestVersion !== result.latestVersion && compareSemVer(plugin.latestVersion, result.latestVersion) < 0) { | ||
49 | plugin.latestVersion = result.latestVersion | ||
50 | await plugin.save() | ||
51 | } | ||
52 | } | ||
53 | } | ||
54 | |||
55 | } | ||
56 | |||
57 | static get Instance () { | ||
58 | return this.instance || (this.instance = new this()) | ||
59 | } | ||
60 | } | ||
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index 8103ec7d3..8cb3153aa 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts | |||
@@ -8,6 +8,7 @@ import { isBooleanValid, isSafePath } from '../../helpers/custom-validators/misc | |||
8 | import { PluginModel } from '../../models/server/plugin' | 8 | import { PluginModel } from '../../models/server/plugin' |
9 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' | 9 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' |
10 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 10 | import { PluginType } from '../../../shared/models/plugins/plugin.type' |
11 | import { CONFIG } from '../../initializers/config' | ||
11 | 12 | ||
12 | const servePluginStaticDirectoryValidator = (pluginType: PluginType) => [ | 13 | const servePluginStaticDirectoryValidator = (pluginType: PluginType) => [ |
13 | param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), | 14 | param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), |
@@ -33,7 +34,7 @@ const servePluginStaticDirectoryValidator = (pluginType: PluginType) => [ | |||
33 | ] | 34 | ] |
34 | 35 | ||
35 | const listPluginsValidator = [ | 36 | const listPluginsValidator = [ |
36 | query('type') | 37 | query('pluginType') |
37 | .optional() | 38 | .optional() |
38 | .custom(isPluginTypeValid).withMessage('Should have a valid plugin type'), | 39 | .custom(isPluginTypeValid).withMessage('Should have a valid plugin type'), |
39 | query('uninstalled') | 40 | query('uninstalled') |
@@ -119,12 +120,39 @@ const updatePluginSettingsValidator = [ | |||
119 | } | 120 | } |
120 | ] | 121 | ] |
121 | 122 | ||
123 | const listAvailablePluginsValidator = [ | ||
124 | query('sort') | ||
125 | .optional() | ||
126 | .exists().withMessage('Should have a valid sort'), | ||
127 | query('search') | ||
128 | .optional() | ||
129 | .exists().withMessage('Should have a valid search'), | ||
130 | query('pluginType') | ||
131 | .optional() | ||
132 | .custom(isPluginTypeValid).withMessage('Should have a valid plugin type'), | ||
133 | |||
134 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
135 | logger.debug('Checking enabledPluginValidator parameters', { parameters: req.query }) | ||
136 | |||
137 | if (areValidationErrors(req, res)) return | ||
138 | |||
139 | if (CONFIG.PLUGINS.INDEX.ENABLED === false) { | ||
140 | return res.status(400) | ||
141 | .json({ error: 'Plugin index is not enabled' }) | ||
142 | .end() | ||
143 | } | ||
144 | |||
145 | return next() | ||
146 | } | ||
147 | ] | ||
148 | |||
122 | // --------------------------------------------------------------------------- | 149 | // --------------------------------------------------------------------------- |
123 | 150 | ||
124 | export { | 151 | export { |
125 | servePluginStaticDirectoryValidator, | 152 | servePluginStaticDirectoryValidator, |
126 | updatePluginSettingsValidator, | 153 | updatePluginSettingsValidator, |
127 | uninstallPluginValidator, | 154 | uninstallPluginValidator, |
155 | listAvailablePluginsValidator, | ||
128 | existingPluginValidator, | 156 | existingPluginValidator, |
129 | installOrUpdatePluginValidator, | 157 | installOrUpdatePluginValidator, |
130 | listPluginsValidator | 158 | listPluginsValidator |
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 102db85cb..c75e701d6 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -22,6 +22,7 @@ const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMN | |||
22 | const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS) | 22 | const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS) |
23 | const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) | 23 | const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) |
24 | const SORTABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PLUGINS) | 24 | const SORTABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PLUGINS) |
25 | const SORTABLE_AVAILABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) | ||
25 | 26 | ||
26 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 27 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
27 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 28 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
@@ -43,6 +44,7 @@ const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUM | |||
43 | const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS) | 44 | const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS) |
44 | const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS) | 45 | const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS) |
45 | const pluginsSortValidator = checkSort(SORTABLE_PLUGINS_COLUMNS) | 46 | const pluginsSortValidator = checkSort(SORTABLE_PLUGINS_COLUMNS) |
47 | const availablePluginsSortValidator = checkSort(SORTABLE_AVAILABLE_PLUGINS_COLUMNS) | ||
46 | 48 | ||
47 | // --------------------------------------------------------------------------- | 49 | // --------------------------------------------------------------------------- |
48 | 50 | ||
@@ -61,6 +63,7 @@ export { | |||
61 | videoCommentThreadsSortValidator, | 63 | videoCommentThreadsSortValidator, |
62 | videoRatesSortValidator, | 64 | videoRatesSortValidator, |
63 | userSubscriptionsSortValidator, | 65 | userSubscriptionsSortValidator, |
66 | availablePluginsSortValidator, | ||
64 | videoChannelsSearchSortValidator, | 67 | videoChannelsSearchSortValidator, |
65 | accountsBlocklistSortValidator, | 68 | accountsBlocklistSortValidator, |
66 | serversBlocklistSortValidator, | 69 | serversBlocklistSortValidator, |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index bd3d7a81e..ba43713f6 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -10,6 +10,7 @@ import { | |||
10 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 10 | import { PluginType } from '../../../shared/models/plugins/plugin.type' |
11 | import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model' | 11 | import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model' |
12 | import { FindAndCountOptions, json } from 'sequelize' | 12 | import { FindAndCountOptions, json } from 'sequelize' |
13 | import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' | ||
13 | 14 | ||
14 | @DefaultScope(() => ({ | 15 | @DefaultScope(() => ({ |
15 | attributes: { | 16 | attributes: { |
@@ -177,7 +178,7 @@ export class PluginModel extends Model<PluginModel> { | |||
177 | } | 178 | } |
178 | 179 | ||
179 | static listForApi (options: { | 180 | static listForApi (options: { |
180 | type?: PluginType, | 181 | pluginType?: PluginType, |
181 | uninstalled?: boolean, | 182 | uninstalled?: boolean, |
182 | start: number, | 183 | start: number, |
183 | count: number, | 184 | count: number, |
@@ -193,7 +194,7 @@ export class PluginModel extends Model<PluginModel> { | |||
193 | } | 194 | } |
194 | } | 195 | } |
195 | 196 | ||
196 | if (options.type) query.where['type'] = options.type | 197 | if (options.pluginType) query.where['type'] = options.pluginType |
197 | 198 | ||
198 | return PluginModel | 199 | return PluginModel |
199 | .findAndCountAll(query) | 200 | .findAndCountAll(query) |
@@ -202,8 +203,18 @@ export class PluginModel extends Model<PluginModel> { | |||
202 | }) | 203 | }) |
203 | } | 204 | } |
204 | 205 | ||
205 | static normalizePluginName (name: string) { | 206 | static listInstalled () { |
206 | return name.replace(/^peertube-((theme)|(plugin))-/, '') | 207 | const query = { |
208 | where: { | ||
209 | uninstalled: false | ||
210 | } | ||
211 | } | ||
212 | |||
213 | return PluginModel.findAll(query) | ||
214 | } | ||
215 | |||
216 | static normalizePluginName (npmName: string) { | ||
217 | return npmName.replace(/^peertube-((theme)|(plugin))-/, '') | ||
207 | } | 218 | } |
208 | 219 | ||
209 | static getTypeFromNpmName (npmName: string) { | 220 | static getTypeFromNpmName (npmName: string) { |
diff --git a/shared/core-utils/miscs/miscs.ts b/shared/core-utils/miscs/miscs.ts index c668e44c1..a3921b568 100644 --- a/shared/core-utils/miscs/miscs.ts +++ b/shared/core-utils/miscs/miscs.ts | |||
@@ -2,6 +2,24 @@ function randomInt (low: number, high: number) { | |||
2 | return Math.floor(Math.random() * (high - low) + low) | 2 | return Math.floor(Math.random() * (high - low) + low) |
3 | } | 3 | } |
4 | 4 | ||
5 | // Thanks https://stackoverflow.com/a/16187766 | ||
6 | function compareSemVer (a: string, b: string) { | ||
7 | const regExStrip0 = /(\.0+)+$/ | ||
8 | const segmentsA = a.replace(regExStrip0, '').split('.') | ||
9 | const segmentsB = b.replace(regExStrip0, '').split('.') | ||
10 | |||
11 | const l = Math.min(segmentsA.length, segmentsB.length) | ||
12 | |||
13 | for (let i = 0; i < l; i++) { | ||
14 | const diff = parseInt(segmentsA[ i ], 10) - parseInt(segmentsB[ i ], 10) | ||
15 | |||
16 | if (diff) return diff | ||
17 | } | ||
18 | |||
19 | return segmentsA.length - segmentsB.length | ||
20 | } | ||
21 | |||
5 | export { | 22 | export { |
6 | randomInt | 23 | randomInt, |
24 | compareSemVer | ||
7 | } | 25 | } |
diff --git a/shared/models/plugins/peertube-plugin-list.model.ts b/shared/models/plugins/peertube-plugin-index-list.model.ts index 5f0ecce68..817bac31e 100644 --- a/shared/models/plugins/peertube-plugin-list.model.ts +++ b/shared/models/plugins/peertube-plugin-index-list.model.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { PluginType } from './plugin.type' | 1 | import { PluginType } from './plugin.type' |
2 | 2 | ||
3 | export interface PeertubePluginList { | 3 | export interface PeertubePluginIndexList { |
4 | start: number | 4 | start: number |
5 | count: number | 5 | count: number |
6 | sort: string | 6 | sort: string |
diff --git a/shared/models/plugins/peertube-plugin-index.model.ts b/shared/models/plugins/peertube-plugin-index.model.ts index 2957a338d..e91c8b4dc 100644 --- a/shared/models/plugins/peertube-plugin-index.model.ts +++ b/shared/models/plugins/peertube-plugin-index.model.ts | |||
@@ -8,4 +8,7 @@ export interface PeerTubePluginIndex { | |||
8 | popularity: number | 8 | popularity: number |
9 | 9 | ||
10 | latestVersion: string | 10 | latestVersion: string |
11 | |||
12 | name?: string | ||
13 | installed?: boolean | ||
11 | } | 14 | } |
diff --git a/shared/models/plugins/peertube-plugin-latest-version.model.ts b/shared/models/plugins/peertube-plugin-latest-version.model.ts index 36dd3af54..dec4618fa 100644 --- a/shared/models/plugins/peertube-plugin-latest-version.model.ts +++ b/shared/models/plugins/peertube-plugin-latest-version.model.ts | |||
@@ -1,5 +1,10 @@ | |||
1 | export interface PeertubePluginLatestVersion { | 1 | export interface PeertubePluginLatestVersionRequest { |
2 | currentPeerTubeEngine?: string, | 2 | currentPeerTubeEngine?: string, |
3 | 3 | ||
4 | npmNames: string[] | 4 | npmNames: string[] |
5 | } | 5 | } |
6 | |||
7 | export type PeertubePluginLatestVersionResponse = { | ||
8 | npmName: string | ||
9 | latestVersion: string | null | ||
10 | }[] | ||