diff options
author | Chocobozzz <me@florianbigard.com> | 2020-06-23 14:10:17 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-06-23 16:00:49 +0200 |
commit | 67ed6552b831df66713bac9e672738796128d33f (patch) | |
tree | 59c97d41e0b49d75a90aa3de987968ab9b1ff447 /client/src/app/shared/shared-instance | |
parent | 0c4bacbff53bc732f5a2677d62a6ead7752e2405 (diff) | |
download | PeerTube-67ed6552b831df66713bac9e672738796128d33f.tar.gz PeerTube-67ed6552b831df66713bac9e672738796128d33f.tar.zst PeerTube-67ed6552b831df66713bac9e672738796128d33f.zip |
Reorganize client shared modules
Diffstat (limited to 'client/src/app/shared/shared-instance')
13 files changed, 656 insertions, 0 deletions
diff --git a/client/src/app/shared/shared-instance/feature-boolean.component.html b/client/src/app/shared/shared-instance/feature-boolean.component.html new file mode 100644 index 000000000..ccb8a30cc --- /dev/null +++ b/client/src/app/shared/shared-instance/feature-boolean.component.html | |||
@@ -0,0 +1,3 @@ | |||
1 | <span *ngIf="value === true" class="glyphicon glyphicon-ok" i18n-aria-label aria-label="yes"></span> | ||
2 | <span *ngIf="value === false" class="glyphicon glyphicon-remove" i18n-aria-label aria-label="no"></span> | ||
3 | |||
diff --git a/client/src/app/shared/shared-instance/feature-boolean.component.scss b/client/src/app/shared/shared-instance/feature-boolean.component.scss new file mode 100644 index 000000000..56d08af06 --- /dev/null +++ b/client/src/app/shared/shared-instance/feature-boolean.component.scss | |||
@@ -0,0 +1,10 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .glyphicon-ok { | ||
5 | color: $green; | ||
6 | } | ||
7 | |||
8 | .glyphicon-remove { | ||
9 | color: $red; | ||
10 | } | ||
diff --git a/client/src/app/shared/shared-instance/feature-boolean.component.ts b/client/src/app/shared/shared-instance/feature-boolean.component.ts new file mode 100644 index 000000000..d02d513d6 --- /dev/null +++ b/client/src/app/shared/shared-instance/feature-boolean.component.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-feature-boolean', | ||
5 | templateUrl: './feature-boolean.component.html', | ||
6 | styleUrls: [ './feature-boolean.component.scss' ] | ||
7 | }) | ||
8 | export class FeatureBooleanComponent { | ||
9 | @Input() value: boolean | ||
10 | } | ||
diff --git a/client/src/app/shared/shared-instance/index.ts b/client/src/app/shared/shared-instance/index.ts new file mode 100644 index 000000000..1aeed357e --- /dev/null +++ b/client/src/app/shared/shared-instance/index.ts | |||
@@ -0,0 +1,6 @@ | |||
1 | export * from './feature-boolean.component' | ||
2 | export * from './instance-features-table.component' | ||
3 | export * from './instance-follow.service' | ||
4 | export * from './instance-statistics.component' | ||
5 | export * from './instance.service' | ||
6 | export * from './shared-instance.module' | ||
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html new file mode 100644 index 000000000..f6a3b7f0b --- /dev/null +++ b/client/src/app/shared/shared-instance/instance-features-table.component.html | |||
@@ -0,0 +1,107 @@ | |||
1 | <div class="feature-table"> | ||
2 | |||
3 | <table class="table" *ngIf="serverConfig"> | ||
4 | <caption i18n>Features found on this instance</caption> | ||
5 | <tr> | ||
6 | <th i18n class="label" scope="row">PeerTube version</th> | ||
7 | |||
8 | <td class="value">{{ getServerVersionAndCommit() }}</td> | ||
9 | </tr> | ||
10 | |||
11 | <tr> | ||
12 | <th i18n class="label" scope="row"> | ||
13 | <div>Default NSFW/sensitive videos policy</div> | ||
14 | <div class="more-info">can be redefined by the users</div> | ||
15 | </th> | ||
16 | |||
17 | <td class="value">{{ buildNSFWLabel() }}</td> | ||
18 | </tr> | ||
19 | |||
20 | <tr> | ||
21 | <th i18n class="label" scope="row">User registration allowed</th> | ||
22 | <td> | ||
23 | <my-feature-boolean [value]="serverConfig.signup.allowed"></my-feature-boolean> | ||
24 | </td> | ||
25 | </tr> | ||
26 | |||
27 | <tr> | ||
28 | <th i18n class="label" colspan="2">Video uploads</th> | ||
29 | </tr> | ||
30 | |||
31 | <tr> | ||
32 | <th i18n class="sub-label" scope="row">Transcoding in multiple resolutions</th> | ||
33 | <td> | ||
34 | <my-feature-boolean [value]="serverConfig.transcoding.enabledResolutions.length !== 0"></my-feature-boolean> | ||
35 | </td> | ||
36 | </tr> | ||
37 | |||
38 | <tr> | ||
39 | <th i18n class="sub-label" scope="row">Video uploads</th> | ||
40 | <td> | ||
41 | <span i18n *ngIf="serverConfig.autoBlacklist.videos.ofUsers.enabled">Requires manual validation by moderators</span> | ||
42 | <span i18n *ngIf="!serverConfig.autoBlacklist.videos.ofUsers.enabled">Automatically published</span> | ||
43 | </td> | ||
44 | </tr> | ||
45 | |||
46 | <tr> | ||
47 | <th i18n class="sub-label" scope="row">Video quota</th> | ||
48 | |||
49 | <td class="value"> | ||
50 | <ng-container *ngIf="initialUserVideoQuota !== -1"> | ||
51 | {{ initialUserVideoQuota | bytes: 0 }} <ng-container *ngIf="dailyUserVideoQuota !== -1">({{ dailyUserVideoQuota | bytes: 0 }} per day)</ng-container> | ||
52 | |||
53 | <my-help tooltipPlacement="auto" helpType="custom"> | ||
54 | <ng-template ptTemplate="customHtml"> | ||
55 | <div [innerHTML]="quotaHelpIndication"></div> | ||
56 | </ng-template> | ||
57 | </my-help> | ||
58 | </ng-container> | ||
59 | |||
60 | <ng-container i18n *ngIf="initialUserVideoQuota === -1"> | ||
61 | Unlimited <ng-container *ngIf="dailyUserVideoQuota !== -1">({{ dailyUserVideoQuota | bytes: 0 }} per day)</ng-container> | ||
62 | </ng-container> | ||
63 | </td> | ||
64 | </tr> | ||
65 | |||
66 | <tr> | ||
67 | <th i18n class="label" colspan="2">Import</th> | ||
68 | </tr> | ||
69 | |||
70 | <tr> | ||
71 | <th i18n class="sub-label" scope="row">HTTP import (YouTube, Vimeo, direct URL...)</th> | ||
72 | <td> | ||
73 | <my-feature-boolean [value]="serverConfig.import.videos.http.enabled"></my-feature-boolean> | ||
74 | </td> | ||
75 | </tr> | ||
76 | |||
77 | <tr> | ||
78 | <th i18n class="sub-label" scope="row">Torrent import</th> | ||
79 | <td> | ||
80 | <my-feature-boolean [value]="serverConfig.import.videos.torrent.enabled"></my-feature-boolean> | ||
81 | </td> | ||
82 | </tr> | ||
83 | |||
84 | |||
85 | <tr> | ||
86 | <th i18n class="label" colspan="2">Player</th> | ||
87 | </tr> | ||
88 | |||
89 | <tr> | ||
90 | <th i18n class="sub-label" scope="row">P2P enabled</th> | ||
91 | <td> | ||
92 | <my-feature-boolean [value]="serverConfig.tracker.enabled"></my-feature-boolean> | ||
93 | </td> | ||
94 | </tr> | ||
95 | |||
96 | <tr> | ||
97 | <th i18n class="label" colspan="2">Search</th> | ||
98 | </tr> | ||
99 | |||
100 | <tr> | ||
101 | <th i18n class="sub-label" scope="row">Users can resolve distant content</th> | ||
102 | <td> | ||
103 | <my-feature-boolean [value]="serverConfig.search.remoteUri.users"></my-feature-boolean> | ||
104 | </td> | ||
105 | </tr> | ||
106 | </table> | ||
107 | </div> | ||
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.scss b/client/src/app/shared/shared-instance/instance-features-table.component.scss new file mode 100644 index 000000000..a51574741 --- /dev/null +++ b/client/src/app/shared/shared-instance/instance-features-table.component.scss | |||
@@ -0,0 +1,40 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | table { | ||
5 | font-size: 14px; | ||
6 | color: pvar(--mainForegroundColor); | ||
7 | |||
8 | .label, | ||
9 | .sub-label { | ||
10 | min-width: 330px; | ||
11 | |||
12 | &.label { | ||
13 | font-weight: $font-semibold; | ||
14 | } | ||
15 | |||
16 | &.sub-label { | ||
17 | font-weight: $font-regular; | ||
18 | padding-left: 30px; | ||
19 | } | ||
20 | |||
21 | .more-info { | ||
22 | font-style: italic; | ||
23 | font-weight: initial; | ||
24 | font-size: 14px | ||
25 | } | ||
26 | } | ||
27 | |||
28 | td { | ||
29 | vertical-align: middle; | ||
30 | } | ||
31 | |||
32 | caption { | ||
33 | caption-side: top; | ||
34 | font-size: 15px; | ||
35 | font-weight: $font-semibold; | ||
36 | color: pvar(--mainForegroundColor); | ||
37 | } | ||
38 | } | ||
39 | |||
40 | |||
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts new file mode 100644 index 000000000..8fd15ebad --- /dev/null +++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts | |||
@@ -0,0 +1,81 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ServerService } from '@app/core' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { ServerConfig } from '@shared/models' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-instance-features-table', | ||
8 | templateUrl: './instance-features-table.component.html', | ||
9 | styleUrls: [ './instance-features-table.component.scss' ] | ||
10 | }) | ||
11 | export class InstanceFeaturesTableComponent implements OnInit { | ||
12 | quotaHelpIndication = '' | ||
13 | serverConfig: ServerConfig | ||
14 | |||
15 | constructor ( | ||
16 | private i18n: I18n, | ||
17 | private serverService: ServerService | ||
18 | ) { | ||
19 | } | ||
20 | |||
21 | get initialUserVideoQuota () { | ||
22 | return this.serverConfig.user.videoQuota | ||
23 | } | ||
24 | |||
25 | get dailyUserVideoQuota () { | ||
26 | return Math.min(this.initialUserVideoQuota, this.serverConfig.user.videoQuotaDaily) | ||
27 | } | ||
28 | |||
29 | ngOnInit () { | ||
30 | this.serverConfig = this.serverService.getTmpConfig() | ||
31 | this.serverService.getConfig() | ||
32 | .subscribe(config => { | ||
33 | this.serverConfig = config | ||
34 | this.buildQuotaHelpIndication() | ||
35 | }) | ||
36 | } | ||
37 | |||
38 | buildNSFWLabel () { | ||
39 | const policy = this.serverConfig.instance.defaultNSFWPolicy | ||
40 | |||
41 | if (policy === 'do_not_list') return this.i18n('Hidden') | ||
42 | if (policy === 'blur') return this.i18n('Blurred with confirmation request') | ||
43 | if (policy === 'display') return this.i18n('Displayed') | ||
44 | } | ||
45 | |||
46 | getServerVersionAndCommit () { | ||
47 | return this.serverService.getServerVersionAndCommit() | ||
48 | } | ||
49 | |||
50 | private getApproximateTime (seconds: number) { | ||
51 | const hours = Math.floor(seconds / 3600) | ||
52 | let pluralSuffix = '' | ||
53 | if (hours > 1) pluralSuffix = 's' | ||
54 | if (hours > 0) return `~ ${hours} hour${pluralSuffix}` | ||
55 | |||
56 | const minutes = Math.floor(seconds % 3600 / 60) | ||
57 | |||
58 | return this.i18n('~ {{minutes}} {minutes, plural, =1 {minute} other {minutes}}', { minutes }) | ||
59 | } | ||
60 | |||
61 | private buildQuotaHelpIndication () { | ||
62 | if (this.initialUserVideoQuota === -1) return | ||
63 | |||
64 | const initialUserVideoQuotaBit = this.initialUserVideoQuota * 8 | ||
65 | |||
66 | // 1080p: ~ 6Mbps | ||
67 | // 720p: ~ 4Mbps | ||
68 | // 360p: ~ 1.5Mbps | ||
69 | const fullHdSeconds = initialUserVideoQuotaBit / (6 * 1000 * 1000) | ||
70 | const hdSeconds = initialUserVideoQuotaBit / (4 * 1000 * 1000) | ||
71 | const normalSeconds = initialUserVideoQuotaBit / (1.5 * 1000 * 1000) | ||
72 | |||
73 | const lines = [ | ||
74 | this.i18n('{{seconds}} of full HD videos', { seconds: this.getApproximateTime(fullHdSeconds) }), | ||
75 | this.i18n('{{seconds}} of HD videos', { seconds: this.getApproximateTime(hdSeconds) }), | ||
76 | this.i18n('{{seconds}} of average quality videos', { seconds: this.getApproximateTime(normalSeconds) }) | ||
77 | ] | ||
78 | |||
79 | this.quotaHelpIndication = lines.join('<br />') | ||
80 | } | ||
81 | } | ||
diff --git a/client/src/app/shared/shared-instance/instance-follow.service.ts b/client/src/app/shared/shared-instance/instance-follow.service.ts new file mode 100644 index 000000000..3c9ccc40f --- /dev/null +++ b/client/src/app/shared/shared-instance/instance-follow.service.ts | |||
@@ -0,0 +1,116 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { Observable } from 'rxjs' | ||
3 | import { catchError, map } from 'rxjs/operators' | ||
4 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
5 | import { Injectable } from '@angular/core' | ||
6 | import { RestExtractor, RestPagination, RestService } from '@app/core' | ||
7 | import { ActivityPubActorType, ActorFollow, FollowState, ResultList } from '@shared/index' | ||
8 | import { environment } from '../../../environments/environment' | ||
9 | |||
10 | @Injectable() | ||
11 | export class InstanceFollowService { | ||
12 | private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/server' | ||
13 | |||
14 | constructor ( | ||
15 | private authHttp: HttpClient, | ||
16 | private restService: RestService, | ||
17 | private restExtractor: RestExtractor | ||
18 | ) { | ||
19 | } | ||
20 | |||
21 | getFollowing (options: { | ||
22 | pagination: RestPagination, | ||
23 | sort: SortMeta, | ||
24 | search?: string, | ||
25 | actorType?: ActivityPubActorType, | ||
26 | state?: FollowState | ||
27 | }): Observable<ResultList<ActorFollow>> { | ||
28 | const { pagination, sort, search, state, actorType } = options | ||
29 | |||
30 | let params = new HttpParams() | ||
31 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
32 | |||
33 | if (search) params = params.append('search', search) | ||
34 | if (state) params = params.append('state', state) | ||
35 | if (actorType) params = params.append('actorType', actorType) | ||
36 | |||
37 | return this.authHttp.get<ResultList<ActorFollow>>(InstanceFollowService.BASE_APPLICATION_URL + '/following', { params }) | ||
38 | .pipe( | ||
39 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
40 | catchError(res => this.restExtractor.handleError(res)) | ||
41 | ) | ||
42 | } | ||
43 | |||
44 | getFollowers (options: { | ||
45 | pagination: RestPagination, | ||
46 | sort: SortMeta, | ||
47 | search?: string, | ||
48 | actorType?: ActivityPubActorType, | ||
49 | state?: FollowState | ||
50 | }): Observable<ResultList<ActorFollow>> { | ||
51 | const { pagination, sort, search, state, actorType } = options | ||
52 | |||
53 | let params = new HttpParams() | ||
54 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
55 | |||
56 | if (search) params = params.append('search', search) | ||
57 | if (state) params = params.append('state', state) | ||
58 | if (actorType) params = params.append('actorType', actorType) | ||
59 | |||
60 | return this.authHttp.get<ResultList<ActorFollow>>(InstanceFollowService.BASE_APPLICATION_URL + '/followers', { params }) | ||
61 | .pipe( | ||
62 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
63 | catchError(res => this.restExtractor.handleError(res)) | ||
64 | ) | ||
65 | } | ||
66 | |||
67 | follow (notEmptyHosts: string[]) { | ||
68 | const body = { | ||
69 | hosts: notEmptyHosts | ||
70 | } | ||
71 | |||
72 | return this.authHttp.post(InstanceFollowService.BASE_APPLICATION_URL + '/following', body) | ||
73 | .pipe( | ||
74 | map(this.restExtractor.extractDataBool), | ||
75 | catchError(res => this.restExtractor.handleError(res)) | ||
76 | ) | ||
77 | } | ||
78 | |||
79 | unfollow (follow: ActorFollow) { | ||
80 | return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + follow.following.host) | ||
81 | .pipe( | ||
82 | map(this.restExtractor.extractDataBool), | ||
83 | catchError(res => this.restExtractor.handleError(res)) | ||
84 | ) | ||
85 | } | ||
86 | |||
87 | acceptFollower (follow: ActorFollow) { | ||
88 | const handle = follow.follower.name + '@' + follow.follower.host | ||
89 | |||
90 | return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/accept`, {}) | ||
91 | .pipe( | ||
92 | map(this.restExtractor.extractDataBool), | ||
93 | catchError(res => this.restExtractor.handleError(res)) | ||
94 | ) | ||
95 | } | ||
96 | |||
97 | rejectFollower (follow: ActorFollow) { | ||
98 | const handle = follow.follower.name + '@' + follow.follower.host | ||
99 | |||
100 | return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/reject`, {}) | ||
101 | .pipe( | ||
102 | map(this.restExtractor.extractDataBool), | ||
103 | catchError(res => this.restExtractor.handleError(res)) | ||
104 | ) | ||
105 | } | ||
106 | |||
107 | removeFollower (follow: ActorFollow) { | ||
108 | const handle = follow.follower.name + '@' + follow.follower.host | ||
109 | |||
110 | return this.authHttp.delete(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}`) | ||
111 | .pipe( | ||
112 | map(this.restExtractor.extractDataBool), | ||
113 | catchError(res => this.restExtractor.handleError(res)) | ||
114 | ) | ||
115 | } | ||
116 | } | ||
diff --git a/client/src/app/shared/shared-instance/instance-statistics.component.html b/client/src/app/shared/shared-instance/instance-statistics.component.html new file mode 100644 index 000000000..399cf10fe --- /dev/null +++ b/client/src/app/shared/shared-instance/instance-statistics.component.html | |||
@@ -0,0 +1,101 @@ | |||
1 | <p i18n *ngIf="null === serverStats">Loading instance statistics...</p> | ||
2 | |||
3 | <section *ngIf="null !== serverStats"> | ||
4 | <h3 i18n>Local</h3> | ||
5 | |||
6 | <div class="row"> | ||
7 | <div class="col-6 col-lg-4 col-xl-3"> | ||
8 | <div class="card stat"> | ||
9 | <div class="card-body"> | ||
10 | <p class="stat-value">{{ serverStats.totalUsers }}</p> | ||
11 | <p class="stat-label" i18n>users</p> | ||
12 | </div> | ||
13 | <i class="glyphicon glyphicon-user icon-bottom"></i> | ||
14 | </div> | ||
15 | </div> | ||
16 | |||
17 | <div class="col-6 col-lg-4 col-xl-3"> | ||
18 | <div class="card stat"> | ||
19 | <div class="card-body"> | ||
20 | <p class="stat-value">{{ serverStats.totalLocalVideos }}</p> | ||
21 | <p class="stat-label" i18n>videos</p> | ||
22 | </div> | ||
23 | <i class="glyphicon glyphicon-facetime-video"></i> | ||
24 | </div> | ||
25 | </div> | ||
26 | |||
27 | <div class="col-6 col-lg-4 col-xl-3"> | ||
28 | <div class="card stat"> | ||
29 | <div class="card-body"> | ||
30 | <p class="stat-value">{{ serverStats.totalLocalVideoViews }}</p> | ||
31 | <p class="stat-label" i18n>video views</p> | ||
32 | </div> | ||
33 | <i class="glyphicon glyphicon-eye-open"></i> | ||
34 | </div> | ||
35 | </div> | ||
36 | |||
37 | <div class="col-6 col-lg-4 col-xl-3"> | ||
38 | <div class="card stat"> | ||
39 | <div class="card-body"> | ||
40 | <p class="stat-value">{{ serverStats.totalLocalVideoComments }}</p> | ||
41 | <p class="stat-label" i18n>video comments</p> | ||
42 | </div> | ||
43 | <i class="glyphicon glyphicon-comment"></i> | ||
44 | </div> | ||
45 | </div> | ||
46 | |||
47 | <div class="col-6 col-lg-4 col-xl-3"> | ||
48 | <div class="card stat"> | ||
49 | <div class="card-body"> | ||
50 | <p class="stat-value">{{ serverStats.totalLocalVideoFilesSize | bytes:1 }}</p> | ||
51 | <p class="stat-label" i18n>of hosted video</p> | ||
52 | </div> | ||
53 | <i class="glyphicon glyphicon-hdd"></i> | ||
54 | </div> | ||
55 | </div> | ||
56 | </div> | ||
57 | |||
58 | <h3 i18n>Federation</h3> | ||
59 | |||
60 | <div class="row"> | ||
61 | <div class="col-6 col-lg-4 col-xl-3"> | ||
62 | <div class="card stat"> | ||
63 | <div class="card-body"> | ||
64 | <p class="stat-value">{{ serverStats.totalVideos }}</p> | ||
65 | <p class="stat-label" i18n>videos</p> | ||
66 | </div> | ||
67 | <i class="glyphicon glyphicon-facetime-video"></i> | ||
68 | </div> | ||
69 | </div> | ||
70 | |||
71 | <div class="col-6 col-lg-4 col-xl-3"> | ||
72 | <div class="card stat"> | ||
73 | <div class="card-body"> | ||
74 | <p class="stat-value">{{ serverStats.totalVideoComments }}</p> | ||
75 | <p class="stat-label" i18n>video comments</p> | ||
76 | </div> | ||
77 | <i class="glyphicon glyphicon-comment"></i> | ||
78 | </div> | ||
79 | </div> | ||
80 | |||
81 | <div class="col-6 col-lg-4 col-xl-3"> | ||
82 | <div class="card stat"> | ||
83 | <div class="card-body"> | ||
84 | <p class="stat-value">{{ serverStats.totalInstanceFollowers }}</p> | ||
85 | <p class="stat-label" i18n>followers</p> | ||
86 | </div> | ||
87 | <i class="glyphicon glyphicon-retweet"></i> | ||
88 | </div> | ||
89 | </div> | ||
90 | |||
91 | <div class="col-6 col-lg-4 col-xl-3"> | ||
92 | <div class="card stat"> | ||
93 | <div class="card-body"> | ||
94 | <p class="stat-value">{{ serverStats.totalInstanceFollowing }}</p> | ||
95 | <p class="stat-label" i18n>following</p> | ||
96 | </div> | ||
97 | <i class="glyphicon glyphicon-retweet"></i> | ||
98 | </div> | ||
99 | </div> | ||
100 | </div> | ||
101 | </section> | ||
diff --git a/client/src/app/shared/shared-instance/instance-statistics.component.scss b/client/src/app/shared/shared-instance/instance-statistics.component.scss new file mode 100644 index 000000000..5286ab03a --- /dev/null +++ b/client/src/app/shared/shared-instance/instance-statistics.component.scss | |||
@@ -0,0 +1,40 @@ | |||
1 | |||
2 | h3 { | ||
3 | font-size: 1.25rem; | ||
4 | } | ||
5 | |||
6 | .stat { | ||
7 | text-align: center; | ||
8 | margin-bottom: 1em; | ||
9 | overflow: hidden; | ||
10 | |||
11 | .stat-value { | ||
12 | font-size: 2.25em; | ||
13 | line-height: 1em; | ||
14 | margin: 0; | ||
15 | } | ||
16 | |||
17 | .stat-label { | ||
18 | font-size: 1.15em; | ||
19 | margin: 0; | ||
20 | } | ||
21 | |||
22 | .glyphicon { | ||
23 | opacity: 0.12; | ||
24 | position: absolute; | ||
25 | left: 16px; | ||
26 | top: -24px; | ||
27 | |||
28 | &.icon-bottom { | ||
29 | top: 4px; | ||
30 | } | ||
31 | |||
32 | &::before { | ||
33 | font-size: 8em; | ||
34 | } | ||
35 | } | ||
36 | |||
37 | .card-body { | ||
38 | z-index: 2; | ||
39 | } | ||
40 | } | ||
diff --git a/client/src/app/shared/shared-instance/instance-statistics.component.ts b/client/src/app/shared/shared-instance/instance-statistics.component.ts new file mode 100644 index 000000000..40aa8a4c0 --- /dev/null +++ b/client/src/app/shared/shared-instance/instance-statistics.component.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ServerStats } from '@shared/models/server' | ||
3 | import { ServerService } from '@app/core' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-instance-statistics', | ||
7 | templateUrl: './instance-statistics.component.html', | ||
8 | styleUrls: [ './instance-statistics.component.scss' ] | ||
9 | }) | ||
10 | export class InstanceStatisticsComponent implements OnInit { | ||
11 | serverStats: ServerStats = null | ||
12 | |||
13 | constructor ( | ||
14 | private serverService: ServerService | ||
15 | ) { | ||
16 | } | ||
17 | |||
18 | ngOnInit () { | ||
19 | this.serverService.getServerStats() | ||
20 | .subscribe(res => this.serverStats = res) | ||
21 | } | ||
22 | } | ||
diff --git a/client/src/app/shared/shared-instance/instance.service.ts b/client/src/app/shared/shared-instance/instance.service.ts new file mode 100644 index 000000000..ba9797bb5 --- /dev/null +++ b/client/src/app/shared/shared-instance/instance.service.ts | |||
@@ -0,0 +1,88 @@ | |||
1 | import { forkJoin } from 'rxjs' | ||
2 | import { catchError, map } from 'rxjs/operators' | ||
3 | import { HttpClient } from '@angular/common/http' | ||
4 | import { Injectable } from '@angular/core' | ||
5 | import { MarkdownService, RestExtractor, ServerService } from '@app/core' | ||
6 | import { About, peertubeTranslate } from '@shared/models' | ||
7 | import { environment } from '../../../environments/environment' | ||
8 | |||
9 | @Injectable() | ||
10 | export class InstanceService { | ||
11 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' | ||
12 | private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server' | ||
13 | |||
14 | constructor ( | ||
15 | private authHttp: HttpClient, | ||
16 | private restExtractor: RestExtractor, | ||
17 | private markdownService: MarkdownService, | ||
18 | private serverService: ServerService | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | getAbout () { | ||
23 | return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about') | ||
24 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
25 | } | ||
26 | |||
27 | contactAdministrator (fromEmail: string, fromName: string, subject: string, message: string) { | ||
28 | const body = { | ||
29 | fromEmail, | ||
30 | fromName, | ||
31 | subject, | ||
32 | body: message | ||
33 | } | ||
34 | |||
35 | return this.authHttp.post(InstanceService.BASE_SERVER_URL + '/contact', body) | ||
36 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
37 | |||
38 | } | ||
39 | |||
40 | async buildHtml (about: About) { | ||
41 | const html = { | ||
42 | description: '', | ||
43 | terms: '', | ||
44 | codeOfConduct: '', | ||
45 | moderationInformation: '', | ||
46 | administrator: '', | ||
47 | hardwareInformation: '' | ||
48 | } | ||
49 | |||
50 | for (const key of Object.keys(html)) { | ||
51 | html[ key ] = await this.markdownService.textMarkdownToHTML(about.instance[ key ]) | ||
52 | } | ||
53 | |||
54 | return html | ||
55 | } | ||
56 | |||
57 | buildTranslatedLanguages (about: About) { | ||
58 | return forkJoin([ | ||
59 | this.serverService.getVideoLanguages(), | ||
60 | this.serverService.getServerLocale() | ||
61 | ]).pipe( | ||
62 | map(([ languagesArray, translations ]) => { | ||
63 | return about.instance.languages | ||
64 | .map(l => { | ||
65 | const languageObj = languagesArray.find(la => la.id === l) | ||
66 | |||
67 | return peertubeTranslate(languageObj.label, translations) | ||
68 | }) | ||
69 | }) | ||
70 | ) | ||
71 | } | ||
72 | |||
73 | buildTranslatedCategories (about: About) { | ||
74 | return forkJoin([ | ||
75 | this.serverService.getVideoCategories(), | ||
76 | this.serverService.getServerLocale() | ||
77 | ]).pipe( | ||
78 | map(([ categoriesArray, translations ]) => { | ||
79 | return about.instance.categories | ||
80 | .map(c => { | ||
81 | const categoryObj = categoriesArray.find(ca => ca.id === c) | ||
82 | |||
83 | return peertubeTranslate(categoryObj.label, translations) | ||
84 | }) | ||
85 | }) | ||
86 | ) | ||
87 | } | ||
88 | } | ||
diff --git a/client/src/app/shared/shared-instance/shared-instance.module.ts b/client/src/app/shared/shared-instance/shared-instance.module.ts new file mode 100644 index 000000000..b75ad1a12 --- /dev/null +++ b/client/src/app/shared/shared-instance/shared-instance.module.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | |||
2 | import { NgModule } from '@angular/core' | ||
3 | import { SharedMainModule } from '../shared-main/shared-main.module' | ||
4 | import { FeatureBooleanComponent } from './feature-boolean.component' | ||
5 | import { InstanceFeaturesTableComponent } from './instance-features-table.component' | ||
6 | import { InstanceFollowService } from './instance-follow.service' | ||
7 | import { InstanceStatisticsComponent } from './instance-statistics.component' | ||
8 | import { InstanceService } from './instance.service' | ||
9 | |||
10 | @NgModule({ | ||
11 | imports: [ | ||
12 | SharedMainModule | ||
13 | ], | ||
14 | |||
15 | declarations: [ | ||
16 | FeatureBooleanComponent, | ||
17 | InstanceFeaturesTableComponent, | ||
18 | InstanceStatisticsComponent | ||
19 | ], | ||
20 | |||
21 | exports: [ | ||
22 | FeatureBooleanComponent, | ||
23 | InstanceFeaturesTableComponent, | ||
24 | InstanceStatisticsComponent | ||
25 | ], | ||
26 | |||
27 | providers: [ | ||
28 | InstanceFollowService, | ||
29 | InstanceService | ||
30 | ] | ||
31 | }) | ||
32 | export class SharedInstanceModule { } | ||