From 67ed6552b831df66713bac9e672738796128d33f Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 23 Jun 2020 14:10:17 +0200 Subject: Reorganize client shared modules --- .../shared-instance/feature-boolean.component.html | 3 + .../shared-instance/feature-boolean.component.scss | 10 ++ .../shared-instance/feature-boolean.component.ts | 10 ++ client/src/app/shared/shared-instance/index.ts | 6 ++ .../instance-features-table.component.html | 107 +++++++++++++++++++ .../instance-features-table.component.scss | 40 +++++++ .../instance-features-table.component.ts | 81 ++++++++++++++ .../shared-instance/instance-follow.service.ts | 116 +++++++++++++++++++++ .../instance-statistics.component.html | 101 ++++++++++++++++++ .../instance-statistics.component.scss | 40 +++++++ .../instance-statistics.component.ts | 22 ++++ .../app/shared/shared-instance/instance.service.ts | 88 ++++++++++++++++ .../shared-instance/shared-instance.module.ts | 32 ++++++ 13 files changed, 656 insertions(+) create mode 100644 client/src/app/shared/shared-instance/feature-boolean.component.html create mode 100644 client/src/app/shared/shared-instance/feature-boolean.component.scss create mode 100644 client/src/app/shared/shared-instance/feature-boolean.component.ts create mode 100644 client/src/app/shared/shared-instance/index.ts create mode 100644 client/src/app/shared/shared-instance/instance-features-table.component.html create mode 100644 client/src/app/shared/shared-instance/instance-features-table.component.scss create mode 100644 client/src/app/shared/shared-instance/instance-features-table.component.ts create mode 100644 client/src/app/shared/shared-instance/instance-follow.service.ts create mode 100644 client/src/app/shared/shared-instance/instance-statistics.component.html create mode 100644 client/src/app/shared/shared-instance/instance-statistics.component.scss create mode 100644 client/src/app/shared/shared-instance/instance-statistics.component.ts create mode 100644 client/src/app/shared/shared-instance/instance.service.ts create mode 100644 client/src/app/shared/shared-instance/shared-instance.module.ts (limited to 'client/src/app/shared/shared-instance') 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 @@ + + + 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 @@ +@import '_variables'; +@import '_mixins'; + +.glyphicon-ok { + color: $green; +} + +.glyphicon-remove { + color: $red; +} 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 @@ +import { Component, Input } from '@angular/core' + +@Component({ + selector: 'my-feature-boolean', + templateUrl: './feature-boolean.component.html', + styleUrls: [ './feature-boolean.component.scss' ] +}) +export class FeatureBooleanComponent { + @Input() value: boolean +} 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 @@ +export * from './feature-boolean.component' +export * from './instance-features-table.component' +export * from './instance-follow.service' +export * from './instance-statistics.component' +export * from './instance.service' +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 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Features found on this instance
PeerTube version{{ getServerVersionAndCommit() }}
+
Default NSFW/sensitive videos policy
+
can be redefined by the users
+
{{ buildNSFWLabel() }}
User registration allowed + +
Video uploads
Transcoding in multiple resolutions + +
Video uploads + Requires manual validation by moderators + Automatically published +
Video quota + + {{ initialUserVideoQuota | bytes: 0 }} ({{ dailyUserVideoQuota | bytes: 0 }} per day) + + + +
+
+
+
+ + + Unlimited ({{ dailyUserVideoQuota | bytes: 0 }} per day) + +
Import
HTTP import (YouTube, Vimeo, direct URL...) + +
Torrent import + +
Player
P2P enabled + +
Search
Users can resolve distant content + +
+
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 @@ +@import '_variables'; +@import '_mixins'; + +table { + font-size: 14px; + color: pvar(--mainForegroundColor); + + .label, + .sub-label { + min-width: 330px; + + &.label { + font-weight: $font-semibold; + } + + &.sub-label { + font-weight: $font-regular; + padding-left: 30px; + } + + .more-info { + font-style: italic; + font-weight: initial; + font-size: 14px + } + } + + td { + vertical-align: middle; + } + + caption { + caption-side: top; + font-size: 15px; + font-weight: $font-semibold; + color: pvar(--mainForegroundColor); + } +} + + 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 @@ +import { Component, OnInit } from '@angular/core' +import { ServerService } from '@app/core' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { ServerConfig } from '@shared/models' + +@Component({ + selector: 'my-instance-features-table', + templateUrl: './instance-features-table.component.html', + styleUrls: [ './instance-features-table.component.scss' ] +}) +export class InstanceFeaturesTableComponent implements OnInit { + quotaHelpIndication = '' + serverConfig: ServerConfig + + constructor ( + private i18n: I18n, + private serverService: ServerService + ) { + } + + get initialUserVideoQuota () { + return this.serverConfig.user.videoQuota + } + + get dailyUserVideoQuota () { + return Math.min(this.initialUserVideoQuota, this.serverConfig.user.videoQuotaDaily) + } + + ngOnInit () { + this.serverConfig = this.serverService.getTmpConfig() + this.serverService.getConfig() + .subscribe(config => { + this.serverConfig = config + this.buildQuotaHelpIndication() + }) + } + + buildNSFWLabel () { + const policy = this.serverConfig.instance.defaultNSFWPolicy + + if (policy === 'do_not_list') return this.i18n('Hidden') + if (policy === 'blur') return this.i18n('Blurred with confirmation request') + if (policy === 'display') return this.i18n('Displayed') + } + + getServerVersionAndCommit () { + return this.serverService.getServerVersionAndCommit() + } + + private getApproximateTime (seconds: number) { + const hours = Math.floor(seconds / 3600) + let pluralSuffix = '' + if (hours > 1) pluralSuffix = 's' + if (hours > 0) return `~ ${hours} hour${pluralSuffix}` + + const minutes = Math.floor(seconds % 3600 / 60) + + return this.i18n('~ {{minutes}} {minutes, plural, =1 {minute} other {minutes}}', { minutes }) + } + + private buildQuotaHelpIndication () { + if (this.initialUserVideoQuota === -1) return + + const initialUserVideoQuotaBit = this.initialUserVideoQuota * 8 + + // 1080p: ~ 6Mbps + // 720p: ~ 4Mbps + // 360p: ~ 1.5Mbps + const fullHdSeconds = initialUserVideoQuotaBit / (6 * 1000 * 1000) + const hdSeconds = initialUserVideoQuotaBit / (4 * 1000 * 1000) + const normalSeconds = initialUserVideoQuotaBit / (1.5 * 1000 * 1000) + + const lines = [ + this.i18n('{{seconds}} of full HD videos', { seconds: this.getApproximateTime(fullHdSeconds) }), + this.i18n('{{seconds}} of HD videos', { seconds: this.getApproximateTime(hdSeconds) }), + this.i18n('{{seconds}} of average quality videos', { seconds: this.getApproximateTime(normalSeconds) }) + ] + + this.quotaHelpIndication = lines.join('
') + } +} 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 @@ +import { SortMeta } from 'primeng/api' +import { Observable } from 'rxjs' +import { catchError, map } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, RestPagination, RestService } from '@app/core' +import { ActivityPubActorType, ActorFollow, FollowState, ResultList } from '@shared/index' +import { environment } from '../../../environments/environment' + +@Injectable() +export class InstanceFollowService { + private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/server' + + constructor ( + private authHttp: HttpClient, + private restService: RestService, + private restExtractor: RestExtractor + ) { + } + + getFollowing (options: { + pagination: RestPagination, + sort: SortMeta, + search?: string, + actorType?: ActivityPubActorType, + state?: FollowState + }): Observable> { + const { pagination, sort, search, state, actorType } = options + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (search) params = params.append('search', search) + if (state) params = params.append('state', state) + if (actorType) params = params.append('actorType', actorType) + + return this.authHttp.get>(InstanceFollowService.BASE_APPLICATION_URL + '/following', { params }) + .pipe( + map(res => this.restExtractor.convertResultListDateToHuman(res)), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + getFollowers (options: { + pagination: RestPagination, + sort: SortMeta, + search?: string, + actorType?: ActivityPubActorType, + state?: FollowState + }): Observable> { + const { pagination, sort, search, state, actorType } = options + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (search) params = params.append('search', search) + if (state) params = params.append('state', state) + if (actorType) params = params.append('actorType', actorType) + + return this.authHttp.get>(InstanceFollowService.BASE_APPLICATION_URL + '/followers', { params }) + .pipe( + map(res => this.restExtractor.convertResultListDateToHuman(res)), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + follow (notEmptyHosts: string[]) { + const body = { + hosts: notEmptyHosts + } + + return this.authHttp.post(InstanceFollowService.BASE_APPLICATION_URL + '/following', body) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + unfollow (follow: ActorFollow) { + return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + follow.following.host) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + acceptFollower (follow: ActorFollow) { + const handle = follow.follower.name + '@' + follow.follower.host + + return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/accept`, {}) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + rejectFollower (follow: ActorFollow) { + const handle = follow.follower.name + '@' + follow.follower.host + + return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/reject`, {}) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + removeFollower (follow: ActorFollow) { + const handle = follow.follower.name + '@' + follow.follower.host + + return this.authHttp.delete(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}`) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } +} 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 @@ +

Loading instance statistics...

+ +
+

Local

+ +
+
+
+
+

{{ serverStats.totalUsers }}

+

users

+
+ +
+
+ +
+
+
+

{{ serverStats.totalLocalVideos }}

+

videos

+
+ +
+
+ +
+
+
+

{{ serverStats.totalLocalVideoViews }}

+

video views

+
+ +
+
+ +
+
+
+

{{ serverStats.totalLocalVideoComments }}

+

video comments

+
+ +
+
+ +
+
+
+

{{ serverStats.totalLocalVideoFilesSize | bytes:1 }}

+

of hosted video

+
+ +
+
+
+ +

Federation

+ +
+
+
+
+

{{ serverStats.totalVideos }}

+

videos

+
+ +
+
+ +
+
+
+

{{ serverStats.totalVideoComments }}

+

video comments

+
+ +
+
+ +
+
+
+

{{ serverStats.totalInstanceFollowers }}

+

followers

+
+ +
+
+ +
+
+
+

{{ serverStats.totalInstanceFollowing }}

+

following

+
+ +
+
+
+
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 @@ + +h3 { + font-size: 1.25rem; +} + +.stat { + text-align: center; + margin-bottom: 1em; + overflow: hidden; + + .stat-value { + font-size: 2.25em; + line-height: 1em; + margin: 0; + } + + .stat-label { + font-size: 1.15em; + margin: 0; + } + + .glyphicon { + opacity: 0.12; + position: absolute; + left: 16px; + top: -24px; + + &.icon-bottom { + top: 4px; + } + + &::before { + font-size: 8em; + } + } + + .card-body { + z-index: 2; + } +} 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 @@ +import { Component, OnInit } from '@angular/core' +import { ServerStats } from '@shared/models/server' +import { ServerService } from '@app/core' + +@Component({ + selector: 'my-instance-statistics', + templateUrl: './instance-statistics.component.html', + styleUrls: [ './instance-statistics.component.scss' ] +}) +export class InstanceStatisticsComponent implements OnInit { + serverStats: ServerStats = null + + constructor ( + private serverService: ServerService + ) { + } + + ngOnInit () { + this.serverService.getServerStats() + .subscribe(res => this.serverStats = res) + } +} 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 @@ +import { forkJoin } from 'rxjs' +import { catchError, map } from 'rxjs/operators' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { MarkdownService, RestExtractor, ServerService } from '@app/core' +import { About, peertubeTranslate } from '@shared/models' +import { environment } from '../../../environments/environment' + +@Injectable() +export class InstanceService { + private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' + private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private markdownService: MarkdownService, + private serverService: ServerService + ) { + } + + getAbout () { + return this.authHttp.get(InstanceService.BASE_CONFIG_URL + '/about') + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + contactAdministrator (fromEmail: string, fromName: string, subject: string, message: string) { + const body = { + fromEmail, + fromName, + subject, + body: message + } + + return this.authHttp.post(InstanceService.BASE_SERVER_URL + '/contact', body) + .pipe(catchError(res => this.restExtractor.handleError(res))) + + } + + async buildHtml (about: About) { + const html = { + description: '', + terms: '', + codeOfConduct: '', + moderationInformation: '', + administrator: '', + hardwareInformation: '' + } + + for (const key of Object.keys(html)) { + html[ key ] = await this.markdownService.textMarkdownToHTML(about.instance[ key ]) + } + + return html + } + + buildTranslatedLanguages (about: About) { + return forkJoin([ + this.serverService.getVideoLanguages(), + this.serverService.getServerLocale() + ]).pipe( + map(([ languagesArray, translations ]) => { + return about.instance.languages + .map(l => { + const languageObj = languagesArray.find(la => la.id === l) + + return peertubeTranslate(languageObj.label, translations) + }) + }) + ) + } + + buildTranslatedCategories (about: About) { + return forkJoin([ + this.serverService.getVideoCategories(), + this.serverService.getServerLocale() + ]).pipe( + map(([ categoriesArray, translations ]) => { + return about.instance.categories + .map(c => { + const categoryObj = categoriesArray.find(ca => ca.id === c) + + return peertubeTranslate(categoryObj.label, translations) + }) + }) + ) + } +} 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 @@ + +import { NgModule } from '@angular/core' +import { SharedMainModule } from '../shared-main/shared-main.module' +import { FeatureBooleanComponent } from './feature-boolean.component' +import { InstanceFeaturesTableComponent } from './instance-features-table.component' +import { InstanceFollowService } from './instance-follow.service' +import { InstanceStatisticsComponent } from './instance-statistics.component' +import { InstanceService } from './instance.service' + +@NgModule({ + imports: [ + SharedMainModule + ], + + declarations: [ + FeatureBooleanComponent, + InstanceFeaturesTableComponent, + InstanceStatisticsComponent + ], + + exports: [ + FeatureBooleanComponent, + InstanceFeaturesTableComponent, + InstanceStatisticsComponent + ], + + providers: [ + InstanceFollowService, + InstanceService + ] +}) +export class SharedInstanceModule { } -- cgit v1.2.3