diff options
author | Chocobozzz <me@florianbigard.com> | 2019-07-16 11:33:22 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-07-24 10:58:16 +0200 |
commit | 6702a1b2ccd666285dee9c72b5bace641d2fce8b (patch) | |
tree | 78b7125d664b6f6b6c993c4e8483e1bdd24a0a30 /client/src/app/+admin | |
parent | 503c6f440abc8f5924c38c4bd63591cb6cefacec (diff) | |
download | PeerTube-6702a1b2ccd666285dee9c72b5bace641d2fce8b.tar.gz PeerTube-6702a1b2ccd666285dee9c72b5bace641d2fce8b.tar.zst PeerTube-6702a1b2ccd666285dee9c72b5bace641d2fce8b.zip |
Add ability to search available plugins
Diffstat (limited to 'client/src/app/+admin')
9 files changed, 247 insertions, 50 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 | } | ||