diff options
Diffstat (limited to 'client/src/app/+admin/plugins/plugin-search')
3 files changed, 184 insertions, 8 deletions
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 | } |