diff options
Diffstat (limited to 'client')
10 files changed, 94 insertions, 8 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 d4501490f..f10b4eb8d 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 | |||
@@ -26,8 +26,11 @@ | |||
26 | <span i18n class="button-label">Homepage</span> | 26 | <span i18n class="button-label">Homepage</span> |
27 | </a> | 27 | </a> |
28 | 28 | ||
29 | <my-edit-button *ngIf="pluginType !== PluginType.THEME" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button> | ||
29 | 30 | ||
30 | <my-edit-button [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button> | 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)" | ||
33 | ></my-button> | ||
31 | 34 | ||
32 | <my-delete-button (click)="uninstall(plugin)" label="Uninstall" i18n-label></my-delete-button> | 35 | <my-delete-button (click)="uninstall(plugin)" label="Uninstall" i18n-label></my-delete-button> |
33 | </div> | 36 | </div> |
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss index f250404ed..7641c507b 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 | |||
@@ -35,3 +35,7 @@ | |||
35 | @include peertube-button-link; | 35 | @include peertube-button-link; |
36 | @include button-with-icon(21px, 0, -2px); | 36 | @include button-with-icon(21px, 0, -2px); |
37 | } | 37 | } |
38 | |||
39 | .update-button[disabled="true"] /deep/ .action-button { | ||
40 | cursor: default !important; | ||
41 | } | ||
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 26a9a616e..67a11c3a8 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,6 +6,7 @@ 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 | 10 | ||
10 | @Component({ | 11 | @Component({ |
11 | selector: 'my-plugin-list-installed', | 12 | selector: 'my-plugin-list-installed', |
@@ -26,6 +27,9 @@ export class PluginListInstalledComponent implements OnInit { | |||
26 | sort = 'name' | 27 | sort = 'name' |
27 | 28 | ||
28 | plugins: PeerTubePlugin[] = [] | 29 | plugins: PeerTubePlugin[] = [] |
30 | updating: { [name: string]: boolean } = {} | ||
31 | |||
32 | PluginType = PluginType | ||
29 | 33 | ||
30 | constructor ( | 34 | constructor ( |
31 | private i18n: I18n, | 35 | private i18n: I18n, |
@@ -49,7 +53,7 @@ export class PluginListInstalledComponent implements OnInit { | |||
49 | this.pagination.currentPage = 1 | 53 | this.pagination.currentPage = 1 |
50 | this.plugins = [] | 54 | this.plugins = [] |
51 | 55 | ||
52 | this.router.navigate([], { queryParams: { pluginType: this.pluginType }}) | 56 | this.router.navigate([], { queryParams: { pluginType: this.pluginType } }) |
53 | 57 | ||
54 | this.loadMorePlugins() | 58 | this.loadMorePlugins() |
55 | } | 59 | } |
@@ -82,6 +86,18 @@ export class PluginListInstalledComponent implements OnInit { | |||
82 | return this.i18n('You don\'t have themes installed yet.') | 86 | return this.i18n('You don\'t have themes installed yet.') |
83 | } | 87 | } |
84 | 88 | ||
89 | isUpdateAvailable (plugin: PeerTubePlugin) { | ||
90 | return plugin.latestVersion && compareSemVer(plugin.latestVersion, plugin.version) > 0 | ||
91 | } | ||
92 | |||
93 | getUpdateLabel (plugin: PeerTubePlugin) { | ||
94 | return this.i18n('Update to {{version}}', { version: plugin.latestVersion }) | ||
95 | } | ||
96 | |||
97 | isUpdating (plugin: PeerTubePlugin) { | ||
98 | return !!this.updating[this.getUpdatingKey(plugin)] | ||
99 | } | ||
100 | |||
85 | async uninstall (plugin: PeerTubePlugin) { | 101 | async uninstall (plugin: PeerTubePlugin) { |
86 | const res = await this.confirmService.confirm( | 102 | const res = await this.confirmService.confirm( |
87 | this.i18n('Do you really want to uninstall {{pluginName}}?', { pluginName: plugin.name }), | 103 | this.i18n('Do you really want to uninstall {{pluginName}}?', { pluginName: plugin.name }), |
@@ -102,7 +118,32 @@ export class PluginListInstalledComponent implements OnInit { | |||
102 | ) | 118 | ) |
103 | } | 119 | } |
104 | 120 | ||
121 | async update (plugin: PeerTubePlugin) { | ||
122 | const updatingKey = this.getUpdatingKey(plugin) | ||
123 | if (this.updating[updatingKey]) return | ||
124 | |||
125 | this.updating[updatingKey] = true | ||
126 | |||
127 | this.pluginService.update(plugin.name, plugin.type) | ||
128 | .pipe() | ||
129 | .subscribe( | ||
130 | res => { | ||
131 | this.updating[updatingKey] = false | ||
132 | |||
133 | this.notifier.success(this.i18n('{{pluginName}} updated.', { pluginName: plugin.name })) | ||
134 | |||
135 | Object.assign(plugin, res) | ||
136 | }, | ||
137 | |||
138 | err => this.notifier.error(err.message) | ||
139 | ) | ||
140 | } | ||
141 | |||
105 | getShowRouterLink (plugin: PeerTubePlugin) { | 142 | getShowRouterLink (plugin: PeerTubePlugin) { |
106 | return [ '/admin', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, plugin.type) ] | 143 | return [ '/admin', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, plugin.type) ] |
107 | } | 144 | } |
145 | |||
146 | private getUpdatingKey (plugin: PeerTubePlugin) { | ||
147 | return plugin.name + plugin.type | ||
148 | } | ||
108 | } | 149 | } |
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 1d33cd179..89f190675 100644 --- a/client/src/app/+admin/plugins/shared/plugin-api.service.ts +++ b/client/src/app/+admin/plugins/shared/plugin-api.service.ts | |||
@@ -9,7 +9,7 @@ import { ComponentPagination } from '@app/shared/rest/component-pagination.model | |||
9 | import { ResultList } from '@shared/models' | 9 | import { ResultList } from '@shared/models' |
10 | import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' | 10 | 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 { InstallPlugin } 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 | 14 | ||
15 | @Injectable() | 15 | @Injectable() |
@@ -89,8 +89,17 @@ export class PluginApiService { | |||
89 | .pipe(catchError(res => this.restExtractor.handleError(res))) | 89 | .pipe(catchError(res => this.restExtractor.handleError(res))) |
90 | } | 90 | } |
91 | 91 | ||
92 | update (pluginName: string, pluginType: PluginType) { | ||
93 | const body: ManagePlugin = { | ||
94 | npmName: this.nameToNpmName(pluginName, pluginType) | ||
95 | } | ||
96 | |||
97 | return this.authHttp.post(PluginApiService.BASE_APPLICATION_URL + '/update', body) | ||
98 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
99 | } | ||
100 | |||
92 | install (npmName: string) { | 101 | install (npmName: string) { |
93 | const body: InstallPlugin = { | 102 | const body: InstallOrUpdatePlugin = { |
94 | npmName | 103 | npmName |
95 | } | 104 | } |
96 | 105 | ||
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index 86bde2d02..c6ba3dd17 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts | |||
@@ -48,7 +48,9 @@ export class PluginService { | |||
48 | .toPromise() | 48 | .toPromise() |
49 | } | 49 | } |
50 | 50 | ||
51 | addPlugin (plugin: ServerConfigPlugin) { | 51 | addPlugin (plugin: ServerConfigPlugin, isTheme = false) { |
52 | const pathPrefix = isTheme ? '/themes' : '/plugins' | ||
53 | |||
52 | for (const key of Object.keys(plugin.clientScripts)) { | 54 | for (const key of Object.keys(plugin.clientScripts)) { |
53 | const clientScript = plugin.clientScripts[key] | 55 | const clientScript = plugin.clientScripts[key] |
54 | 56 | ||
@@ -58,7 +60,7 @@ export class PluginService { | |||
58 | this.scopes[scope].push({ | 60 | this.scopes[scope].push({ |
59 | plugin, | 61 | plugin, |
60 | clientScript: { | 62 | clientScript: { |
61 | script: environment.apiUrl + `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`, | 63 | script: environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`, |
62 | scopes: clientScript.scopes | 64 | scopes: clientScript.scopes |
63 | } | 65 | } |
64 | }) | 66 | }) |
diff --git a/client/src/app/shared/buttons/button.component.html b/client/src/app/shared/buttons/button.component.html index b6df67102..d2b0eb81a 100644 --- a/client/src/app/shared/buttons/button.component.html +++ b/client/src/app/shared/buttons/button.component.html | |||
@@ -1,4 +1,6 @@ | |||
1 | <span class="action-button" [ngClass]="className" [title]="getTitle()"> | 1 | <span class="action-button" [ngClass]="className" [title]="getTitle()"> |
2 | <my-global-icon [iconName]="icon"></my-global-icon> | 2 | <my-global-icon *ngIf="!loading" [iconName]="icon"></my-global-icon> |
3 | <my-small-loader [loading]="loading"></my-small-loader> | ||
4 | |||
3 | <span class="button-label">{{ label }}</span> | 5 | <span class="button-label">{{ label }}</span> |
4 | </span> | 6 | </span> |
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss index 99d7f51c1..4cc2b0573 100644 --- a/client/src/app/shared/buttons/button.component.scss +++ b/client/src/app/shared/buttons/button.component.scss | |||
@@ -1,6 +1,12 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | my-small-loader /deep/ .root { | ||
5 | display: inline-block; | ||
6 | margin: 0 3px 0 0; | ||
7 | width: 20px; | ||
8 | } | ||
9 | |||
4 | .action-button { | 10 | .action-button { |
5 | @include peertube-button-link; | 11 | @include peertube-button-link; |
6 | @include button-with-icon(21px, 0, -2px); | 12 | @include button-with-icon(21px, 0, -2px); |
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts index cf334e8d5..cac5ad210 100644 --- a/client/src/app/shared/buttons/button.component.ts +++ b/client/src/app/shared/buttons/button.component.ts | |||
@@ -12,6 +12,7 @@ export class ButtonComponent { | |||
12 | @Input() className = 'grey-button' | 12 | @Input() className = 'grey-button' |
13 | @Input() icon: GlobalIconName = undefined | 13 | @Input() icon: GlobalIconName = undefined |
14 | @Input() title: string = undefined | 14 | @Input() title: string = undefined |
15 | @Input() loading = false | ||
15 | 16 | ||
16 | getTitle () { | 17 | getTitle () { |
17 | return this.title || this.label | 18 | return this.title || this.label |
diff --git a/client/src/app/shared/misc/small-loader.component.html b/client/src/app/shared/misc/small-loader.component.html index 5a7cea738..7886f8918 100644 --- a/client/src/app/shared/misc/small-loader.component.html +++ b/client/src/app/shared/misc/small-loader.component.html | |||
@@ -1,3 +1,3 @@ | |||
1 | <div *ngIf="loading"> | 1 | <div class="root" *ngIf="loading"> |
2 | <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div> | 2 | <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div> |
3 | </div> | 3 | </div> |
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts index 85fc1c3a0..098496d45 100644 --- a/client/src/app/shared/misc/utils.ts +++ b/client/src/app/shared/misc/utils.ts | |||
@@ -134,6 +134,23 @@ 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 | |||
137 | export { | 154 | export { |
138 | sortBy, | 155 | sortBy, |
139 | durationToString, | 156 | durationToString, |
@@ -144,6 +161,7 @@ export { | |||
144 | getAbsoluteAPIUrl, | 161 | getAbsoluteAPIUrl, |
145 | dateToHuman, | 162 | dateToHuman, |
146 | immutableAssign, | 163 | immutableAssign, |
164 | compareSemVer, | ||
147 | objectToFormData, | 165 | objectToFormData, |
148 | objectLineFeedToHtml, | 166 | objectLineFeedToHtml, |
149 | removeElementFromArray, | 167 | removeElementFromArray, |