diff options
Diffstat (limited to 'client/src/app')
44 files changed, 605 insertions, 122 deletions
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts index 87beb13da..c8c156105 100644 --- a/client/src/app/+about/about-instance/about-instance.component.ts +++ b/client/src/app/+about/about-instance/about-instance.component.ts | |||
@@ -1,12 +1,10 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { Notifier, ServerService } from '@app/core' | 2 | import { Notifier, ServerService } from '@app/core' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' | 3 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' |
5 | import { InstanceService } from '@app/shared/instance/instance.service' | 4 | import { InstanceService } from '@app/shared/instance/instance.service' |
6 | import { MarkdownService } from '@app/shared/renderer' | ||
7 | import { forkJoin } from 'rxjs' | ||
8 | import { map, switchMap } from 'rxjs/operators' | ||
9 | import { ServerConfig } from '@shared/models' | 5 | import { ServerConfig } from '@shared/models' |
6 | import { ActivatedRoute } from '@angular/router' | ||
7 | import { ResolverData } from './about-instance.resolver' | ||
10 | 8 | ||
11 | @Component({ | 9 | @Component({ |
12 | selector: 'my-about-instance', | 10 | selector: 'my-about-instance', |
@@ -37,11 +35,10 @@ export class AboutInstanceComponent implements OnInit { | |||
37 | serverConfig: ServerConfig | 35 | serverConfig: ServerConfig |
38 | 36 | ||
39 | constructor ( | 37 | constructor ( |
38 | private route: ActivatedRoute, | ||
40 | private notifier: Notifier, | 39 | private notifier: Notifier, |
41 | private serverService: ServerService, | 40 | private serverService: ServerService, |
42 | private instanceService: InstanceService, | 41 | private instanceService: InstanceService |
43 | private markdownService: MarkdownService, | ||
44 | private i18n: I18n | ||
45 | ) {} | 42 | ) {} |
46 | 43 | ||
47 | get instanceName () { | 44 | get instanceName () { |
@@ -56,35 +53,23 @@ export class AboutInstanceComponent implements OnInit { | |||
56 | return this.serverConfig.instance.isNSFW | 53 | return this.serverConfig.instance.isNSFW |
57 | } | 54 | } |
58 | 55 | ||
59 | ngOnInit () { | 56 | async ngOnInit () { |
60 | this.serverConfig = this.serverService.getTmpConfig() | 57 | this.serverConfig = this.serverService.getTmpConfig() |
61 | this.serverService.getConfig() | 58 | this.serverService.getConfig() |
62 | .subscribe(config => this.serverConfig = config) | 59 | .subscribe(config => this.serverConfig = config) |
63 | 60 | ||
64 | this.instanceService.getAbout() | 61 | const { about, languages, categories }: ResolverData = this.route.snapshot.data.instanceData |
65 | .pipe( | 62 | |
66 | switchMap(about => { | 63 | this.languages = languages |
67 | return forkJoin([ | 64 | this.categories = categories |
68 | this.instanceService.buildTranslatedLanguages(about), | 65 | |
69 | this.instanceService.buildTranslatedCategories(about) | 66 | this.shortDescription = about.instance.shortDescription |
70 | ]).pipe(map(([ languages, categories ]) => ({ about, languages, categories }))) | 67 | |
71 | }) | 68 | this.creationReason = about.instance.creationReason |
72 | ).subscribe( | 69 | this.maintenanceLifetime = about.instance.maintenanceLifetime |
73 | async ({ about, languages, categories }) => { | 70 | this.businessModel = about.instance.businessModel |
74 | this.languages = languages | 71 | |
75 | this.categories = categories | 72 | this.html = await this.instanceService.buildHtml(about) |
76 | |||
77 | this.shortDescription = about.instance.shortDescription | ||
78 | |||
79 | this.creationReason = about.instance.creationReason | ||
80 | this.maintenanceLifetime = about.instance.maintenanceLifetime | ||
81 | this.businessModel = about.instance.businessModel | ||
82 | |||
83 | this.html = await this.instanceService.buildHtml(about) | ||
84 | }, | ||
85 | |||
86 | () => this.notifier.error(this.i18n('Cannot get about information from server')) | ||
87 | ) | ||
88 | } | 73 | } |
89 | 74 | ||
90 | openContactModal () { | 75 | openContactModal () { |
diff --git a/client/src/app/+about/about-instance/about-instance.resolver.ts b/client/src/app/+about/about-instance/about-instance.resolver.ts new file mode 100644 index 000000000..94c6abe5a --- /dev/null +++ b/client/src/app/+about/about-instance/about-instance.resolver.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' | ||
3 | import { map, switchMap } from 'rxjs/operators' | ||
4 | import { forkJoin } from 'rxjs' | ||
5 | import { InstanceService } from '@app/shared/instance/instance.service' | ||
6 | import { About } from '@shared/models/server' | ||
7 | |||
8 | export type ResolverData = { about: About, languages: string[], categories: string[] } | ||
9 | |||
10 | @Injectable() | ||
11 | export class AboutInstanceResolver implements Resolve<any> { | ||
12 | constructor ( | ||
13 | private instanceService: InstanceService | ||
14 | ) {} | ||
15 | |||
16 | resolve (route: ActivatedRouteSnapshot) { | ||
17 | return this.instanceService.getAbout() | ||
18 | .pipe( | ||
19 | switchMap(about => { | ||
20 | return forkJoin([ | ||
21 | this.instanceService.buildTranslatedLanguages(about), | ||
22 | this.instanceService.buildTranslatedCategories(about) | ||
23 | ]).pipe(map(([ languages, categories ]) => ({ about, languages, categories }))) | ||
24 | }) | ||
25 | ) | ||
26 | } | ||
27 | } | ||
diff --git a/client/src/app/+about/about-routing.module.ts b/client/src/app/+about/about-routing.module.ts index 33e5070cb..91ccb846f 100644 --- a/client/src/app/+about/about-routing.module.ts +++ b/client/src/app/+about/about-routing.module.ts | |||
@@ -5,6 +5,7 @@ import { AboutComponent } from './about.component' | |||
5 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' | 5 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' |
6 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' | 6 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' |
7 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' | 7 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' |
8 | import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver' | ||
8 | 9 | ||
9 | const aboutRoutes: Routes = [ | 10 | const aboutRoutes: Routes = [ |
10 | { | 11 | { |
@@ -24,6 +25,9 @@ const aboutRoutes: Routes = [ | |||
24 | meta: { | 25 | meta: { |
25 | title: 'About this instance' | 26 | title: 'About this instance' |
26 | } | 27 | } |
28 | }, | ||
29 | resolve: { | ||
30 | instanceData: AboutInstanceResolver | ||
27 | } | 31 | } |
28 | }, | 32 | }, |
29 | { | 33 | { |
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts index 14bf76e55..84d697540 100644 --- a/client/src/app/+about/about.module.ts +++ b/client/src/app/+about/about.module.ts | |||
@@ -7,6 +7,7 @@ import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertub | |||
7 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' | 7 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' |
8 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' | 8 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' |
9 | import { AboutPeertubeContributorsComponent } from '@app/+about/about-peertube/about-peertube-contributors.component' | 9 | import { AboutPeertubeContributorsComponent } from '@app/+about/about-peertube/about-peertube-contributors.component' |
10 | import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver' | ||
10 | 11 | ||
11 | @NgModule({ | 12 | @NgModule({ |
12 | imports: [ | 13 | imports: [ |
@@ -28,6 +29,7 @@ import { AboutPeertubeContributorsComponent } from '@app/+about/about-peertube/a | |||
28 | ], | 29 | ], |
29 | 30 | ||
30 | providers: [ | 31 | providers: [ |
32 | AboutInstanceResolver | ||
31 | ] | 33 | ] |
32 | }) | 34 | }) |
33 | export class AboutModule { } | 35 | export class AboutModule { } |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index ad611f221..a8157de0e 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -96,7 +96,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
96 | } | 96 | } |
97 | 97 | ||
98 | subscribersDisplayFor (count: number) { | 98 | subscribersDisplayFor (count: number) { |
99 | return this.i18n(`{count, plural, =1 {1 subscriber} other {${count} subscribers}}`, { count }) | 99 | return this.i18n('{count, plural, =1 {1 subscriber} other {{{count}} subscribers}}', { count }) |
100 | } | 100 | } |
101 | 101 | ||
102 | private getUserIfNeeded (account: Account) { | 102 | private getUserIfNeeded (account: Account) { |
diff --git a/client/src/app/+admin/admin.component.html b/client/src/app/+admin/admin.component.html index 9a3d90c18..0d06aaedc 100644 --- a/client/src/app/+admin/admin.component.html +++ b/client/src/app/+admin/admin.component.html | |||
@@ -5,7 +5,7 @@ | |||
5 | </a> | 5 | </a> |
6 | 6 | ||
7 | <a i18n *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page"> | 7 | <a i18n *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page"> |
8 | Manage follows | 8 | Follows & redundancies |
9 | </a> | 9 | </a> |
10 | 10 | ||
11 | <a i18n *ngIf="hasVideoAbusesRight() || hasVideoBlacklistRight()" routerLink="/admin/moderation" routerLinkActive="active" class="title-page"> | 11 | <a i18n *ngIf="hasVideoAbusesRight() || hasVideoBlacklistRight()" routerLink="/admin/moderation" routerLinkActive="active" class="title-page"> |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 9c56b5750..fdbe70314 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -5,7 +5,7 @@ import { TableModule } from 'primeng/table' | |||
5 | import { SharedModule } from '../shared' | 5 | import { SharedModule } from '../shared' |
6 | import { AdminRoutingModule } from './admin-routing.module' | 6 | import { AdminRoutingModule } from './admin-routing.module' |
7 | import { AdminComponent } from './admin.component' | 7 | import { AdminComponent } from './admin.component' |
8 | import { FollowersListComponent, FollowingAddComponent, FollowsComponent } from './follows' | 8 | import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' |
9 | import { FollowingListComponent } from './follows/following-list/following-list.component' | 9 | import { FollowingListComponent } from './follows/following-list/following-list.component' |
10 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' | 10 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' |
11 | import { | 11 | import { |
@@ -16,7 +16,6 @@ import { | |||
16 | } from './moderation' | 16 | } from './moderation' |
17 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' | 17 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' |
18 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' | 18 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' |
19 | import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' | ||
20 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' | 19 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' |
21 | import { JobsComponent } from '@app/+admin/system/jobs/jobs.component' | 20 | import { JobsComponent } from '@app/+admin/system/jobs/jobs.component' |
22 | import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system' | 21 | import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system' |
@@ -27,13 +26,18 @@ import { PluginSearchComponent } from '@app/+admin/plugins/plugin-search/plugin- | |||
27 | import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component' | 26 | import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component' |
28 | import { SelectButtonModule } from 'primeng/selectbutton' | 27 | import { SelectButtonModule } from 'primeng/selectbutton' |
29 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' | 28 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' |
29 | import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component' | ||
30 | import { ChartModule } from 'primeng/chart' | ||
30 | 31 | ||
31 | @NgModule({ | 32 | @NgModule({ |
32 | imports: [ | 33 | imports: [ |
33 | AdminRoutingModule, | 34 | AdminRoutingModule, |
35 | |||
36 | SharedModule, | ||
37 | |||
34 | TableModule, | 38 | TableModule, |
35 | SelectButtonModule, | 39 | SelectButtonModule, |
36 | SharedModule | 40 | ChartModule |
37 | ], | 41 | ], |
38 | 42 | ||
39 | declarations: [ | 43 | declarations: [ |
@@ -44,6 +48,8 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' | |||
44 | FollowersListComponent, | 48 | FollowersListComponent, |
45 | FollowingListComponent, | 49 | FollowingListComponent, |
46 | RedundancyCheckboxComponent, | 50 | RedundancyCheckboxComponent, |
51 | VideoRedundanciesListComponent, | ||
52 | VideoRedundancyInformationComponent, | ||
47 | 53 | ||
48 | UsersComponent, | 54 | UsersComponent, |
49 | UserCreateComponent, | 55 | UserCreateComponent, |
@@ -78,7 +84,6 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' | |||
78 | ], | 84 | ], |
79 | 85 | ||
80 | providers: [ | 86 | providers: [ |
81 | RedundancyService, | ||
82 | JobService, | 87 | JobService, |
83 | LogsService, | 88 | LogsService, |
84 | DebugService, | 89 | DebugService, |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 915d60090..d806ea355 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html | |||
@@ -234,6 +234,9 @@ | |||
234 | inputName="signupEnabled" formControlName="enabled" | 234 | inputName="signupEnabled" formControlName="enabled" |
235 | i18n-labelText labelText="Signup enabled" | 235 | i18n-labelText labelText="Signup enabled" |
236 | > | 236 | > |
237 | <ng-container ngProjectAs="description"> | ||
238 | <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span> | ||
239 | </ng-container> | ||
237 | <ng-container ngProjectAs="extra"> | 240 | <ng-container ngProjectAs="extra"> |
238 | <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" | 241 | <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" |
239 | inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" | 242 | inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" |
@@ -243,10 +246,11 @@ | |||
243 | <div [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" class="mt-3"> | 246 | <div [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" class="mt-3"> |
244 | <label i18n for="signupLimit">Signup limit</label> | 247 | <label i18n for="signupLimit">Signup limit</label> |
245 | <input | 248 | <input |
246 | type="text" id="signupLimit" | 249 | type="number" min="-1" id="signupLimit" |
247 | formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }" | 250 | formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }" |
248 | > | 251 | > |
249 | <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div> | 252 | <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div> |
253 | <small *ngIf="form.value['signup']['limit'] === -1" class="text-muted">Signup won't be limited to a fixed number of users.</small> | ||
250 | </div> | 254 | </div> |
251 | </ng-container> | 255 | </ng-container> |
252 | </my-peertube-checkbox> | 256 | </my-peertube-checkbox> |
@@ -318,7 +322,7 @@ | |||
318 | i18n-labelText labelText="Blacklist new videos automatically" | 322 | i18n-labelText labelText="Blacklist new videos automatically" |
319 | > | 323 | > |
320 | <ng-container ngProjectAs="description"> | 324 | <ng-container ngProjectAs="description"> |
321 | <span i18n>Videos of regular users will stay private until a moderator reviews them. Can be overriden per user.</span> | 325 | <span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span> |
322 | </ng-container> | 326 | </ng-container> |
323 | </my-peertube-checkbox> | 327 | </my-peertube-checkbox> |
324 | </div> | 328 | </div> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss index 60d608028..dd70f1c06 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss | |||
@@ -10,6 +10,11 @@ input[type=text] { | |||
10 | display: block; | 10 | display: block; |
11 | } | 11 | } |
12 | 12 | ||
13 | input[type=number] { | ||
14 | @include peertube-input-text(315px); | ||
15 | display: block; | ||
16 | } | ||
17 | |||
13 | input[type=checkbox] { | 18 | input[type=checkbox] { |
14 | @include peertube-checkbox(1px); | 19 | @include peertube-checkbox(1px); |
15 | } | 20 | } |
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.scss b/client/src/app/+admin/follows/following-add/following-add.component.scss index 1baddc95f..df104c14e 100644 --- a/client/src/app/+admin/follows/following-add/following-add.component.scss +++ b/client/src/app/+admin/follows/following-add/following-add.component.scss | |||
@@ -7,7 +7,7 @@ textarea { | |||
7 | 7 | ||
8 | .form-control { | 8 | .form-control { |
9 | &, &:focus { | 9 | &, &:focus { |
10 | background-color: var(--inputColor); | 10 | background-color: var(--inputBackgroundColor); |
11 | color: var(--mainForegroundColor); | 11 | color: var(--mainForegroundColor); |
12 | } | 12 | } |
13 | } | 13 | } |
diff --git a/client/src/app/+admin/follows/follows.component.html b/client/src/app/+admin/follows/follows.component.html index 21d477132..46581daf9 100644 --- a/client/src/app/+admin/follows/follows.component.html +++ b/client/src/app/+admin/follows/follows.component.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <div class="admin-sub-header"> | 1 | <div class="admin-sub-header"> |
2 | <div i18n class="form-sub-title">Manage follows</div> | 2 | <div i18n class="form-sub-title">Follows & redundancies</div> |
3 | 3 | ||
4 | <div class="admin-sub-nav"> | 4 | <div class="admin-sub-nav"> |
5 | <a i18n routerLink="following-list" routerLinkActive="active">Following</a> | 5 | <a i18n routerLink="following-list" routerLinkActive="active">Following</a> |
@@ -7,7 +7,9 @@ | |||
7 | <a i18n routerLink="following-add" routerLinkActive="active">Follow</a> | 7 | <a i18n routerLink="following-add" routerLinkActive="active">Follow</a> |
8 | 8 | ||
9 | <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a> | 9 | <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a> |
10 | |||
11 | <a i18n routerLink="video-redundancies-list" routerLinkActive="active">Video redundancies</a> | ||
10 | </div> | 12 | </div> |
11 | </div> | 13 | </div> |
12 | 14 | ||
13 | <router-outlet></router-outlet> \ No newline at end of file | 15 | <router-outlet></router-outlet> |
diff --git a/client/src/app/+admin/follows/follows.routes.ts b/client/src/app/+admin/follows/follows.routes.ts index e84c79e82..298733eb0 100644 --- a/client/src/app/+admin/follows/follows.routes.ts +++ b/client/src/app/+admin/follows/follows.routes.ts | |||
@@ -6,6 +6,7 @@ import { FollowingAddComponent } from './following-add' | |||
6 | import { FollowersListComponent } from './followers-list' | 6 | import { FollowersListComponent } from './followers-list' |
7 | import { UserRight } from '../../../../../shared' | 7 | import { UserRight } from '../../../../../shared' |
8 | import { FollowingListComponent } from './following-list/following-list.component' | 8 | import { FollowingListComponent } from './following-list/following-list.component' |
9 | import { VideoRedundanciesListComponent } from '@app/+admin/follows/video-redundancies-list' | ||
9 | 10 | ||
10 | export const FollowsRoutes: Routes = [ | 11 | export const FollowsRoutes: Routes = [ |
11 | { | 12 | { |
@@ -47,6 +48,10 @@ export const FollowsRoutes: Routes = [ | |||
47 | title: 'Add follow' | 48 | title: 'Add follow' |
48 | } | 49 | } |
49 | } | 50 | } |
51 | }, | ||
52 | { | ||
53 | path: 'video-redundancies-list', | ||
54 | component: VideoRedundanciesListComponent | ||
50 | } | 55 | } |
51 | ] | 56 | ] |
52 | } | 57 | } |
diff --git a/client/src/app/+admin/follows/index.ts b/client/src/app/+admin/follows/index.ts index e94f33710..4fcb35cb1 100644 --- a/client/src/app/+admin/follows/index.ts +++ b/client/src/app/+admin/follows/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export * from './following-add' | 1 | export * from './following-add' |
2 | export * from './followers-list' | 2 | export * from './followers-list' |
3 | export * from './following-list' | 3 | export * from './following-list' |
4 | export * from './video-redundancies-list' | ||
4 | export * from './follows.component' | 5 | export * from './follows.component' |
5 | export * from './follows.routes' | 6 | export * from './follows.routes' |
diff --git a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts index fa1da26bf..9d7883d97 100644 --- a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts +++ b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' | 4 | import { RedundancyService } from '@app/shared/video/redundancy.service' |
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'my-redundancy-checkbox', | 7 | selector: 'my-redundancy-checkbox', |
diff --git a/client/src/app/+admin/follows/shared/redundancy.service.ts b/client/src/app/+admin/follows/shared/redundancy.service.ts deleted file mode 100644 index 87ae01c04..000000000 --- a/client/src/app/+admin/follows/shared/redundancy.service.ts +++ /dev/null | |||
@@ -1,28 +0,0 @@ | |||
1 | import { catchError, map } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor } from '@app/shared' | ||
5 | import { environment } from '../../../../environments/environment' | ||
6 | |||
7 | @Injectable() | ||
8 | export class RedundancyService { | ||
9 | static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/server/redundancy' | ||
10 | |||
11 | constructor ( | ||
12 | private authHttp: HttpClient, | ||
13 | private restExtractor: RestExtractor | ||
14 | ) { } | ||
15 | |||
16 | updateRedundancy (host: string, redundancyAllowed: boolean) { | ||
17 | const url = RedundancyService.BASE_USER_SUBSCRIPTIONS_URL + '/' + host | ||
18 | |||
19 | const body = { redundancyAllowed } | ||
20 | |||
21 | return this.authHttp.put(url, body) | ||
22 | .pipe( | ||
23 | map(this.restExtractor.extractDataBool), | ||
24 | catchError(err => this.restExtractor.handleError(err)) | ||
25 | ) | ||
26 | } | ||
27 | |||
28 | } | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/index.ts b/client/src/app/+admin/follows/video-redundancies-list/index.ts new file mode 100644 index 000000000..6a7c7f483 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-redundancies-list.component' | |||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html new file mode 100644 index 000000000..80c66ec60 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html | |||
@@ -0,0 +1,82 @@ | |||
1 | <div class="admin-sub-header"> | ||
2 | <div i18n class="form-sub-title">Video redundancies list</div> | ||
3 | |||
4 | <div class="select-filter-block"> | ||
5 | <label for="displayType" i18n>Display</label> | ||
6 | |||
7 | <div class="peertube-select-container"> | ||
8 | <select id="displayType" name="displayType" [(ngModel)]="displayType" (ngModelChange)="onDisplayTypeChanged()"> | ||
9 | <option value="my-videos">My videos duplicated by remote instances</option> | ||
10 | <option value="remote-videos">Remote videos duplicated by my instance</option> | ||
11 | </select> | ||
12 | </div> | ||
13 | </div> | ||
14 | </div> | ||
15 | |||
16 | <p-table | ||
17 | [value]="videoRedundancies" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
18 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" | ||
19 | > | ||
20 | <ng-template pTemplate="header"> | ||
21 | <tr> | ||
22 | <th i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th> | ||
23 | <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> | ||
24 | <th i18n>Video URL</th> | ||
25 | <th i18n *ngIf="isDisplayingRemoteVideos()">Total size</th> | ||
26 | <th></th> | ||
27 | </tr> | ||
28 | </ng-template> | ||
29 | |||
30 | <ng-template pTemplate="body" let-redundancy> | ||
31 | <tr class="expander" [pRowToggler]="redundancy"> | ||
32 | <td *ngIf="isDisplayingRemoteVideos()">{{ getRedundancyStrategy(redundancy) }}</td> | ||
33 | |||
34 | <td>{{ redundancy.name }}</td> | ||
35 | |||
36 | <td> | ||
37 | <a target="_blank" rel="noopener noreferrer" [href]="redundancy.url">{{ redundancy.url }}</a> | ||
38 | </td> | ||
39 | |||
40 | <td *ngIf="isDisplayingRemoteVideos()">{{ getTotalSize(redundancy) | bytes: 1 }}</td> | ||
41 | |||
42 | <td class="action-cell"> | ||
43 | <my-delete-button (click)="removeRedundancy(redundancy)"></my-delete-button> | ||
44 | </td> | ||
45 | </tr> | ||
46 | </ng-template> | ||
47 | |||
48 | <ng-template pTemplate="rowexpansion" let-redundancy> | ||
49 | <tr> | ||
50 | <td colspan="2"> | ||
51 | <div *ngFor="let file of redundancy.redundancies.files" class="expansion-block"> | ||
52 | <my-video-redundancy-information [redundancyElement]="file"></my-video-redundancy-information> | ||
53 | </div> | ||
54 | </td> | ||
55 | </tr> | ||
56 | |||
57 | <tr> | ||
58 | <td colspan="2"> | ||
59 | <div *ngFor="let playlist of redundancy.redundancies.streamingPlaylists"> | ||
60 | <my-video-redundancy-information [redundancyElement]="playlist"></my-video-redundancy-information> | ||
61 | </div> | ||
62 | </td> | ||
63 | </tr> | ||
64 | </ng-template> | ||
65 | </p-table> | ||
66 | |||
67 | |||
68 | <div class="redundancies-charts" *ngIf="isDisplayingRemoteVideos()"> | ||
69 | <div class="form-sub-title" i18n>Enabled strategies stats</div> | ||
70 | |||
71 | <div class="chart-blocks"> | ||
72 | |||
73 | <div *ngIf="noRedundancies" i18n class="no-results"> | ||
74 | No redundancy strategy is enabled on your instance. | ||
75 | </div> | ||
76 | |||
77 | <div class="chart-block" *ngFor="let r of redundanciesGraphsData"> | ||
78 | <p-chart type="pie" [data]="r.graphData" [options]="r.options" width="300px" height="300px"></p-chart> | ||
79 | </div> | ||
80 | |||
81 | </div> | ||
82 | </div> | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss new file mode 100644 index 000000000..05018c281 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss | |||
@@ -0,0 +1,37 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .expansion-block { | ||
5 | margin-bottom: 20px; | ||
6 | } | ||
7 | |||
8 | .admin-sub-header { | ||
9 | align-items: flex-end; | ||
10 | |||
11 | .select-filter-block { | ||
12 | &:not(:last-child) { | ||
13 | margin-right: 10px; | ||
14 | } | ||
15 | |||
16 | label { | ||
17 | margin-bottom: 2px; | ||
18 | } | ||
19 | |||
20 | .peertube-select-container { | ||
21 | @include peertube-select-container(auto); | ||
22 | } | ||
23 | } | ||
24 | } | ||
25 | |||
26 | .redundancies-charts { | ||
27 | margin-top: 50px; | ||
28 | |||
29 | .chart-blocks { | ||
30 | display: flex; | ||
31 | justify-content: center; | ||
32 | |||
33 | .chart-block { | ||
34 | margin: 0 20px; | ||
35 | } | ||
36 | } | ||
37 | } | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts new file mode 100644 index 000000000..4b41d1d86 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts | |||
@@ -0,0 +1,178 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Notifier, ServerService } from '@app/core' | ||
3 | import { SortMeta } from 'primeng/api' | ||
4 | import { ConfirmService } from '../../../core/confirm/confirm.service' | ||
5 | import { RestPagination, RestTable } from '../../../shared' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | ||
8 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' | ||
9 | import { VideosRedundancyStats } from '@shared/models/server' | ||
10 | import { BytesPipe } from 'ngx-pipes' | ||
11 | import { RedundancyService } from '@app/shared/video/redundancy.service' | ||
12 | |||
13 | @Component({ | ||
14 | selector: 'my-video-redundancies-list', | ||
15 | templateUrl: './video-redundancies-list.component.html', | ||
16 | styleUrls: [ './video-redundancies-list.component.scss' ] | ||
17 | }) | ||
18 | export class VideoRedundanciesListComponent extends RestTable implements OnInit { | ||
19 | private static LOCAL_STORAGE_DISPLAY_TYPE = 'video-redundancies-list-display-type' | ||
20 | |||
21 | videoRedundancies: VideoRedundancy[] = [] | ||
22 | totalRecords = 0 | ||
23 | rowsPerPage = 10 | ||
24 | |||
25 | sort: SortMeta = { field: 'name', order: 1 } | ||
26 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
27 | displayType: VideoRedundanciesTarget = 'my-videos' | ||
28 | |||
29 | redundanciesGraphsData: { stats: VideosRedundancyStats, graphData: object, options: object }[] = [] | ||
30 | |||
31 | noRedundancies = false | ||
32 | |||
33 | private bytesPipe: BytesPipe | ||
34 | |||
35 | constructor ( | ||
36 | private notifier: Notifier, | ||
37 | private confirmService: ConfirmService, | ||
38 | private redundancyService: RedundancyService, | ||
39 | private serverService: ServerService, | ||
40 | private i18n: I18n | ||
41 | ) { | ||
42 | super() | ||
43 | |||
44 | this.bytesPipe = new BytesPipe() | ||
45 | } | ||
46 | |||
47 | ngOnInit () { | ||
48 | this.loadSelectLocalStorage() | ||
49 | |||
50 | this.initialize() | ||
51 | |||
52 | this.serverService.getServerStats() | ||
53 | .subscribe(res => { | ||
54 | const redundancies = res.videosRedundancy | ||
55 | |||
56 | if (redundancies.length === 0) this.noRedundancies = true | ||
57 | |||
58 | for (const r of redundancies) { | ||
59 | this.buildPieData(r) | ||
60 | } | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | isDisplayingRemoteVideos () { | ||
65 | return this.displayType === 'remote-videos' | ||
66 | } | ||
67 | |||
68 | getTotalSize (redundancy: VideoRedundancy) { | ||
69 | return redundancy.redundancies.files.reduce((a, b) => a + b.size, 0) + | ||
70 | redundancy.redundancies.streamingPlaylists.reduce((a, b) => a + b.size, 0) | ||
71 | } | ||
72 | |||
73 | onDisplayTypeChanged () { | ||
74 | this.pagination.start = 0 | ||
75 | this.saveSelectLocalStorage() | ||
76 | |||
77 | this.loadData() | ||
78 | } | ||
79 | |||
80 | getRedundancyStrategy (redundancy: VideoRedundancy) { | ||
81 | if (redundancy.redundancies.files.length !== 0) return redundancy.redundancies.files[0].strategy | ||
82 | if (redundancy.redundancies.streamingPlaylists.length !== 0) return redundancy.redundancies.streamingPlaylists[0].strategy | ||
83 | |||
84 | return '' | ||
85 | } | ||
86 | |||
87 | buildPieData (stats: VideosRedundancyStats) { | ||
88 | const totalSize = stats.totalSize | ||
89 | ? stats.totalSize - stats.totalUsed | ||
90 | : stats.totalUsed | ||
91 | |||
92 | if (totalSize === 0) return | ||
93 | |||
94 | this.redundanciesGraphsData.push({ | ||
95 | stats, | ||
96 | graphData: { | ||
97 | labels: [ this.i18n('Used'), this.i18n('Available') ], | ||
98 | datasets: [ | ||
99 | { | ||
100 | data: [ stats.totalUsed, totalSize ], | ||
101 | backgroundColor: [ | ||
102 | '#FF6384', | ||
103 | '#36A2EB' | ||
104 | ], | ||
105 | hoverBackgroundColor: [ | ||
106 | '#FF6384', | ||
107 | '#36A2EB' | ||
108 | ] | ||
109 | } | ||
110 | ] | ||
111 | }, | ||
112 | options: { | ||
113 | title: { | ||
114 | display: true, | ||
115 | text: stats.strategy | ||
116 | }, | ||
117 | |||
118 | tooltips: { | ||
119 | callbacks: { | ||
120 | label: (tooltipItem: any, data: any) => { | ||
121 | const dataset = data.datasets[tooltipItem.datasetIndex] | ||
122 | let label = data.labels[tooltipItem.index] | ||
123 | if (label) label += ': ' | ||
124 | else label = '' | ||
125 | |||
126 | label += this.bytesPipe.transform(dataset.data[tooltipItem.index], 1) | ||
127 | return label | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | } | ||
132 | }) | ||
133 | } | ||
134 | |||
135 | async removeRedundancy (redundancy: VideoRedundancy) { | ||
136 | const message = this.i18n('Do you really want to remove this video redundancy?') | ||
137 | const res = await this.confirmService.confirm(message, this.i18n('Remove redundancy')) | ||
138 | if (res === false) return | ||
139 | |||
140 | this.redundancyService.removeVideoRedundancies(redundancy) | ||
141 | .subscribe( | ||
142 | () => { | ||
143 | this.notifier.success(this.i18n('Video redundancies removed!')) | ||
144 | this.loadData() | ||
145 | }, | ||
146 | |||
147 | err => this.notifier.error(err.message) | ||
148 | ) | ||
149 | |||
150 | } | ||
151 | |||
152 | protected loadData () { | ||
153 | const options = { | ||
154 | pagination: this.pagination, | ||
155 | sort: this.sort, | ||
156 | target: this.displayType | ||
157 | } | ||
158 | |||
159 | this.redundancyService.listVideoRedundancies(options) | ||
160 | .subscribe( | ||
161 | resultList => { | ||
162 | this.videoRedundancies = resultList.data | ||
163 | this.totalRecords = resultList.total | ||
164 | }, | ||
165 | |||
166 | err => this.notifier.error(err.message) | ||
167 | ) | ||
168 | } | ||
169 | |||
170 | private loadSelectLocalStorage () { | ||
171 | const displayType = peertubeLocalStorage.getItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE) | ||
172 | if (displayType) this.displayType = displayType as VideoRedundanciesTarget | ||
173 | } | ||
174 | |||
175 | private saveSelectLocalStorage () { | ||
176 | peertubeLocalStorage.setItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE, this.displayType) | ||
177 | } | ||
178 | } | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html new file mode 100644 index 000000000..a379520e3 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html | |||
@@ -0,0 +1,24 @@ | |||
1 | <div> | ||
2 | <span class="label">Url</span> | ||
3 | <a target="_blank" rel="noopener noreferrer" [href]="redundancyElement.fileUrl">{{ redundancyElement.fileUrl }}</a> | ||
4 | </div> | ||
5 | |||
6 | <div> | ||
7 | <span class="label">Created on</span> | ||
8 | <span>{{ redundancyElement.createdAt | date: 'medium' }}</span> | ||
9 | </div> | ||
10 | |||
11 | <div> | ||
12 | <span class="label">Expires on</span> | ||
13 | <span>{{ redundancyElement.expiresOn | date: 'medium' }}</span> | ||
14 | </div> | ||
15 | |||
16 | <div> | ||
17 | <span class="label">Size</span> | ||
18 | <span>{{ redundancyElement.size | bytes: 1 }}</span> | ||
19 | </div> | ||
20 | |||
21 | <div *ngIf="redundancyElement.strategy"> | ||
22 | <span class="label">Strategy</span> | ||
23 | <span>{{ redundancyElement.strategy }}</span> | ||
24 | </div> | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss new file mode 100644 index 000000000..6b09fbb01 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss | |||
@@ -0,0 +1,8 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .label { | ||
5 | display: inline-block; | ||
6 | min-width: 100px; | ||
7 | font-weight: $font-semibold; | ||
8 | } | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts new file mode 100644 index 000000000..6f3090c08 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { FileRedundancyInformation, StreamingPlaylistRedundancyInformation } from '@shared/models' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-video-redundancy-information', | ||
6 | templateUrl: './video-redundancy-information.component.html', | ||
7 | styleUrls: [ './video-redundancy-information.component.scss' ] | ||
8 | }) | ||
9 | export class VideoRedundancyInformationComponent { | ||
10 | @Input() redundancyElement: FileRedundancyInformation | StreamingPlaylistRedundancyInformation | ||
11 | } | ||
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts index 20c8ea71a..bc40452cf 100644 --- a/client/src/app/+admin/system/jobs/jobs.component.ts +++ b/client/src/app/+admin/system/jobs/jobs.component.ts | |||
@@ -16,8 +16,8 @@ import { JobTypeClient } from '../../../../types/job-type-client.type' | |||
16 | styleUrls: [ './jobs.component.scss' ] | 16 | styleUrls: [ './jobs.component.scss' ] |
17 | }) | 17 | }) |
18 | export class JobsComponent extends RestTable implements OnInit { | 18 | export class JobsComponent extends RestTable implements OnInit { |
19 | private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state' | 19 | private static LOCAL_STORAGE_STATE = 'jobs-list-state' |
20 | private static JOB_STATE_LOCAL_STORAGE_TYPE = 'jobs-list-type' | 20 | private static LOCAL_STORAGE_TYPE = 'jobs-list-type' |
21 | 21 | ||
22 | jobState: JobStateClient = 'waiting' | 22 | jobState: JobStateClient = 'waiting' |
23 | jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ] | 23 | jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ] |
@@ -34,7 +34,8 @@ export class JobsComponent extends RestTable implements OnInit { | |||
34 | 'video-file-import', | 34 | 'video-file-import', |
35 | 'video-import', | 35 | 'video-import', |
36 | 'videos-views', | 36 | 'videos-views', |
37 | 'activitypub-refresher' | 37 | 'activitypub-refresher', |
38 | 'video-redundancy' | ||
38 | ] | 39 | ] |
39 | 40 | ||
40 | jobs: Job[] = [] | 41 | jobs: Job[] = [] |
@@ -77,15 +78,15 @@ export class JobsComponent extends RestTable implements OnInit { | |||
77 | } | 78 | } |
78 | 79 | ||
79 | private loadJobStateAndType () { | 80 | private loadJobStateAndType () { |
80 | const state = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE) | 81 | const state = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_STATE) |
81 | if (state) this.jobState = state as JobState | 82 | if (state) this.jobState = state as JobState |
82 | 83 | ||
83 | const type = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE) | 84 | const type = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_TYPE) |
84 | if (type) this.jobType = type as JobType | 85 | if (type) this.jobType = type as JobType |
85 | } | 86 | } |
86 | 87 | ||
87 | private saveJobStateAndType () { | 88 | private saveJobStateAndType () { |
88 | peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState) | 89 | peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_STATE, this.jobState) |
89 | peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE, this.jobType) | 90 | peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_TYPE, this.jobType) |
90 | } | 91 | } |
91 | } | 92 | } |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts index 7479442d1..355cb4f55 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts | |||
@@ -9,7 +9,7 @@ export abstract class MyAccountVideoChannelEdit extends FormReactive { | |||
9 | abstract isCreation (): boolean | 9 | abstract isCreation (): boolean |
10 | abstract getFormButtonTitle (): string | 10 | abstract getFormButtonTitle (): string |
11 | 11 | ||
12 | // FIXME: We need this method so angular does not complain in the child template | 12 | // We need this method so angular does not complain in child template that doesn't need this |
13 | onAvatarChange (formData: FormData) { /* empty */ } | 13 | onAvatarChange (formData: FormData) { /* empty */ } |
14 | 14 | ||
15 | // Should be implemented by the child | 15 | // Should be implemented by the child |
diff --git a/client/src/app/+signup/+register/register-step-channel.component.html b/client/src/app/+signup/+register/register-step-channel.component.html index 88ff6e3ff..170c2964e 100644 --- a/client/src/app/+signup/+register/register-step-channel.component.html +++ b/client/src/app/+signup/+register/register-step-channel.component.html | |||
@@ -40,7 +40,7 @@ | |||
40 | </div> | 40 | </div> |
41 | 41 | ||
42 | <div class="name-information" i18n> | 42 | <div class="name-information" i18n> |
43 | The channel name is a unique identifier of your channel on this instance. It's like an address mail, so other people can find your channel. | 43 | The channel name is a unique identifier of your channel on this and all the other instances. It's as unique as an email address, which makes it easy for other people to interact with it. |
44 | </div> | 44 | </div> |
45 | 45 | ||
46 | <div *ngIf="formErrors.name" class="form-error"> | 46 | <div *ngIf="formErrors.name" class="form-error"> |
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html index a2a657660..6bac4e4a4 100644 --- a/client/src/app/+signup/+register/register-step-user.component.html +++ b/client/src/app/+signup/+register/register-step-user.component.html | |||
@@ -29,7 +29,7 @@ | |||
29 | </div> | 29 | </div> |
30 | 30 | ||
31 | <div class="name-information" i18n> | 31 | <div class="name-information" i18n> |
32 | The username is a unique identifier of your account on this instance. It's like an address mail, so other people can find you. | 32 | The username is a unique identifier of your account on this and all the other instances. It's as unique as an email address, which makes it easy for other people to interact with it. |
33 | </div> | 33 | </div> |
34 | 34 | ||
35 | <div *ngIf="formErrors.username" class="form-error"> | 35 | <div *ngIf="formErrors.username" class="form-error"> |
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index f4fe14662..f32a892a4 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts | |||
@@ -72,7 +72,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
72 | .getVideoChannelVideos(this.videoChannel, newPagination, this.sort) | 72 | .getVideoChannelVideos(this.videoChannel, newPagination, this.sort) |
73 | .pipe( | 73 | .pipe( |
74 | tap(({ total }) => { | 74 | tap(({ total }) => { |
75 | this.titlePage = this.i18n(`{total, plural, =1 {Published 1 video} other {Published ${total} videos}}`, { total }) | 75 | this.titlePage = this.i18n(`{total, plural, =1 {Published 1 video} other {Published {{total}} videos}}`, { total }) |
76 | }) | 76 | }) |
77 | ) | 77 | ) |
78 | } | 78 | } |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index dda705811..62915ec54 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -48,8 +48,6 @@ export function metaFactory (serverService: ServerService): MetaLoader { | |||
48 | ], | 48 | ], |
49 | imports: [ | 49 | imports: [ |
50 | BrowserModule, | 50 | BrowserModule, |
51 | // FIXME: https://github.com/maxisam/ngx-clipboard/issues/133 | ||
52 | ClipboardModule, | ||
53 | 51 | ||
54 | CoreModule, | 52 | CoreModule, |
55 | SharedModule, | 53 | SharedModule, |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index cdcbcb528..1f6cfb596 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -9,6 +9,7 @@ import { VideoConstant } from '../../../../../shared/models/videos' | |||
9 | import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' | 9 | import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' |
10 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | 10 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' |
11 | import { sortBy } from '@app/shared/misc/utils' | 11 | import { sortBy } from '@app/shared/misc/utils' |
12 | import { ServerStats } from '@shared/models/server' | ||
12 | 13 | ||
13 | @Injectable() | 14 | @Injectable() |
14 | export class ServerService { | 15 | export class ServerService { |
@@ -16,6 +17,8 @@ export class ServerService { | |||
16 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' | 17 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' |
17 | private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/' | 18 | private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/' |
18 | private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' | 19 | private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' |
20 | private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats' | ||
21 | |||
19 | private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' | 22 | private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' |
20 | 23 | ||
21 | configReloaded = new Subject<void>() | 24 | configReloaded = new Subject<void>() |
@@ -235,6 +238,10 @@ export class ServerService { | |||
235 | return this.localeObservable.pipe(first()) | 238 | return this.localeObservable.pipe(first()) |
236 | } | 239 | } |
237 | 240 | ||
241 | getServerStats () { | ||
242 | return this.http.get<ServerStats>(ServerService.BASE_STATS_URL) | ||
243 | } | ||
244 | |||
238 | private loadAttributeEnum <T extends string | number> ( | 245 | private loadAttributeEnum <T extends string | number> ( |
239 | baseUrl: string, | 246 | baseUrl: string, |
240 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', | 247 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', |
diff --git a/client/src/app/search/search-filters.component.html b/client/src/app/search/search-filters.component.html index 07fb2c048..c275285d5 100644 --- a/client/src/app/search/search-filters.component.html +++ b/client/src/app/search/search-filters.component.html | |||
@@ -103,7 +103,7 @@ | |||
103 | </button> | 103 | </button> |
104 | <div class="peertube-select-container"> | 104 | <div class="peertube-select-container"> |
105 | <select id="category" name="category" [(ngModel)]="advancedSearch.categoryOneOf"> | 105 | <select id="category" name="category" [(ngModel)]="advancedSearch.categoryOneOf"> |
106 | <option [value]="undefined" i18n>Any or no category set</option> | 106 | <option [value]="undefined" i18n>Display all categories</option> |
107 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> | 107 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> |
108 | </select> | 108 | </select> |
109 | </div> | 109 | </div> |
@@ -116,7 +116,7 @@ | |||
116 | </button> | 116 | </button> |
117 | <div class="peertube-select-container"> | 117 | <div class="peertube-select-container"> |
118 | <select id="licence" name="licence" [(ngModel)]="advancedSearch.licenceOneOf"> | 118 | <select id="licence" name="licence" [(ngModel)]="advancedSearch.licenceOneOf"> |
119 | <option [value]="undefined" i18n>Any or no license set</option> | 119 | <option [value]="undefined" i18n>Display all licenses</option> |
120 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> | 120 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> |
121 | </select> | 121 | </select> |
122 | </div> | 122 | </div> |
@@ -129,7 +129,7 @@ | |||
129 | </button> | 129 | </button> |
130 | <div class="peertube-select-container"> | 130 | <div class="peertube-select-container"> |
131 | <select id="language" name="language" [(ngModel)]="advancedSearch.languageOneOf"> | 131 | <select id="language" name="language" [(ngModel)]="advancedSearch.languageOneOf"> |
132 | <option [value]="undefined" i18n>Any or no language set</option> | 132 | <option [value]="undefined" i18n>Display all languages</option> |
133 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> | 133 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> |
134 | </select> | 134 | </select> |
135 | </div> | 135 | </div> |
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss index 2a8cfc748..3ccfefd7e 100644 --- a/client/src/app/shared/buttons/button.component.scss +++ b/client/src/app/shared/buttons/button.component.scss | |||
@@ -10,11 +10,26 @@ my-small-loader ::ng-deep .root { | |||
10 | .action-button { | 10 | .action-button { |
11 | @include peertube-button-link; | 11 | @include peertube-button-link; |
12 | @include button-with-icon(21px, 0, -2px); | 12 | @include button-with-icon(21px, 0, -2px); |
13 | } | ||
13 | 14 | ||
14 | // FIXME: Firefox does not apply global .orange-button icon color | 15 | .orange-button { |
15 | &.orange-button { | 16 | @include peertube-button; |
16 | @include apply-svg-color(#fff) | 17 | @include orange-button; |
17 | } | 18 | } |
19 | |||
20 | .orange-button-link { | ||
21 | @include peertube-button-link; | ||
22 | @include orange-button; | ||
23 | } | ||
24 | |||
25 | .grey-button { | ||
26 | @include peertube-button; | ||
27 | @include grey-button; | ||
28 | } | ||
29 | |||
30 | .grey-button-link { | ||
31 | @include peertube-button-link; | ||
32 | @include grey-button; | ||
18 | } | 33 | } |
19 | 34 | ||
20 | // In a table, try to minimize the space taken by this button | 35 | // In a table, try to minimize the space taken by this button |
diff --git a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts index 767e3f026..d20754d11 100644 --- a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts | |||
@@ -56,7 +56,7 @@ export class CustomConfigValidatorsService { | |||
56 | } | 56 | } |
57 | 57 | ||
58 | this.SIGNUP_LIMIT = { | 58 | this.SIGNUP_LIMIT = { |
59 | VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], | 59 | VALIDATORS: [ Validators.required, Validators.min(-1), Validators.pattern('-?[0-9]+') ], |
60 | MESSAGES: { | 60 | MESSAGES: { |
61 | 'required': this.i18n('Signup limit is required.'), | 61 | 'required': this.i18n('Signup limit is required.'), |
62 | 'min': this.i18n('Signup limit must be greater than 1.'), | 62 | 'min': this.i18n('Signup limit must be greater than 1.'), |
diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts index 806aca347..b6e641228 100644 --- a/client/src/app/shared/images/global-icon.component.ts +++ b/client/src/app/shared/images/global-icon.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' | 1 | import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' |
2 | import { HooksService } from '@app/core/plugins/hooks.service' | 2 | import { HooksService } from '@app/core/plugins/hooks.service' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | 3 | ||
5 | const icons = { | 4 | const icons = { |
6 | 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'), | 5 | 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'), |
diff --git a/client/src/app/shared/instance/instance-statistics.component.ts b/client/src/app/shared/instance/instance-statistics.component.ts index 8ec728f05..40aa8a4c0 100644 --- a/client/src/app/shared/instance/instance-statistics.component.ts +++ b/client/src/app/shared/instance/instance-statistics.component.ts | |||
@@ -1,9 +1,6 @@ | |||
1 | import { map } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
5 | import { ServerStats } from '@shared/models/server' | 2 | import { ServerStats } from '@shared/models/server' |
6 | import { environment } from '../../../environments/environment' | 3 | import { ServerService } from '@app/core' |
7 | 4 | ||
8 | @Component({ | 5 | @Component({ |
9 | selector: 'my-instance-statistics', | 6 | selector: 'my-instance-statistics', |
@@ -11,27 +8,15 @@ import { environment } from '../../../environments/environment' | |||
11 | styleUrls: [ './instance-statistics.component.scss' ] | 8 | styleUrls: [ './instance-statistics.component.scss' ] |
12 | }) | 9 | }) |
13 | export class InstanceStatisticsComponent implements OnInit { | 10 | export class InstanceStatisticsComponent implements OnInit { |
14 | private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats' | ||
15 | |||
16 | serverStats: ServerStats = null | 11 | serverStats: ServerStats = null |
17 | 12 | ||
18 | constructor ( | 13 | constructor ( |
19 | private http: HttpClient, | 14 | private serverService: ServerService |
20 | private i18n: I18n | ||
21 | ) { | 15 | ) { |
22 | } | 16 | } |
23 | 17 | ||
24 | ngOnInit () { | 18 | ngOnInit () { |
25 | this.getStats() | 19 | this.serverService.getServerStats() |
26 | .subscribe( | 20 | .subscribe(res => this.serverStats = res) |
27 | res => { | ||
28 | this.serverStats = res | ||
29 | } | ||
30 | ) | ||
31 | } | ||
32 | |||
33 | getStats () { | ||
34 | return this.http | ||
35 | .get<ServerStats>(InstanceStatisticsComponent.BASE_STATS_URL) | ||
36 | } | 21 | } |
37 | } | 22 | } |
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.ts b/client/src/app/shared/menu/top-menu-dropdown.component.ts index 5ccdafb54..24a083654 100644 --- a/client/src/app/shared/menu/top-menu-dropdown.component.ts +++ b/client/src/app/shared/menu/top-menu-dropdown.component.ts | |||
@@ -49,8 +49,7 @@ export class TopMenuDropdownComponent implements OnInit, OnDestroy { | |||
49 | e => e.children && e.children.some(c => !!c.iconName) | 49 | e => e.children && e.children.some(c => !!c.iconName) |
50 | ) | 50 | ) |
51 | 51 | ||
52 | // FIXME: We have to set body for the container to avoid because of scroll overflow on mobile view | 52 | // We have to set body for the container to avoid scroll overflow on mobile view |
53 | // But this break our hovering system | ||
54 | if (this.screen.isInMobileView()) { | 53 | if (this.screen.isInMobileView()) { |
55 | this.container = 'body' | 54 | this.container = 'body' |
56 | } | 55 | } |
diff --git a/client/src/app/shared/renderer/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts index 0d3fde537..f0c87326f 100644 --- a/client/src/app/shared/renderer/markdown.service.ts +++ b/client/src/app/shared/renderer/markdown.service.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { MarkdownIt } from 'markdown-it' | ||
3 | import { buildVideoLink } from '../../../assets/player/utils' | 2 | import { buildVideoLink } from '../../../assets/player/utils' |
4 | import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service' | 3 | import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service' |
4 | import * as MarkdownIt from 'markdown-it' | ||
5 | 5 | ||
6 | type MarkdownParsers = { | 6 | type MarkdownParsers = { |
7 | textMarkdownIt: MarkdownIt | 7 | textMarkdownIt: MarkdownIt |
@@ -100,7 +100,7 @@ export class MarkdownService { | |||
100 | } | 100 | } |
101 | 101 | ||
102 | private async createMarkdownIt (config: MarkdownConfig) { | 102 | private async createMarkdownIt (config: MarkdownConfig) { |
103 | // FIXME: import('...') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function | 103 | // FIXME: import('...') returns a struct module, containing a "default" field |
104 | const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default | 104 | const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default |
105 | 105 | ||
106 | const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html }) | 106 | const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html }) |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index b2eb13f73..d06d37d8c 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -98,6 +98,7 @@ import { FollowService } from '@app/shared/instance/follow.service' | |||
98 | import { MultiSelectModule } from 'primeng/multiselect' | 98 | import { MultiSelectModule } from 'primeng/multiselect' |
99 | import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' | 99 | import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' |
100 | import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' | 100 | import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' |
101 | import { RedundancyService } from '@app/shared/video/redundancy.service' | ||
101 | 102 | ||
102 | @NgModule({ | 103 | @NgModule({ |
103 | imports: [ | 104 | imports: [ |
@@ -300,6 +301,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop | |||
300 | UserNotificationService, | 301 | UserNotificationService, |
301 | 302 | ||
302 | FollowService, | 303 | FollowService, |
304 | RedundancyService, | ||
303 | 305 | ||
304 | I18n | 306 | I18n |
305 | ] | 307 | ] |
diff --git a/client/src/app/shared/video/infinite-scroller.directive.ts b/client/src/app/shared/video/infinite-scroller.directive.ts index 9f613c5fa..f09c3d1fc 100644 --- a/client/src/app/shared/video/infinite-scroller.directive.ts +++ b/client/src/app/shared/video/infinite-scroller.directive.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { distinctUntilChanged, filter, map, share, startWith, tap, throttleTime } from 'rxjs/operators' | 1 | import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators' |
2 | import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' | 2 | import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' |
3 | import { fromEvent, Observable, Subscription } from 'rxjs' | 3 | import { fromEvent, Observable, Subscription } from 'rxjs' |
4 | 4 | ||
@@ -53,7 +53,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterConten | |||
53 | const scrollableElement = this.onItself ? this.container : window | 53 | const scrollableElement = this.onItself ? this.container : window |
54 | const scrollObservable = fromEvent(scrollableElement, 'scroll') | 54 | const scrollObservable = fromEvent(scrollableElement, 'scroll') |
55 | .pipe( | 55 | .pipe( |
56 | startWith(null as string), // FIXME: typings | 56 | startWith(true), |
57 | throttleTime(200, undefined, throttleOptions), | 57 | throttleTime(200, undefined, throttleOptions), |
58 | map(() => this.getScrollInfo()), | 58 | map(() => this.getScrollInfo()), |
59 | distinctUntilChanged((o1, o2) => o1.current === o2.current), | 59 | distinctUntilChanged((o1, o2) => o1.current === o2.current), |
diff --git a/client/src/app/shared/video/redundancy.service.ts b/client/src/app/shared/video/redundancy.service.ts new file mode 100644 index 000000000..fb918d73b --- /dev/null +++ b/client/src/app/shared/video/redundancy.service.ts | |||
@@ -0,0 +1,73 @@ | |||
1 | import { catchError, map, toArray } from 'rxjs/operators' | ||
2 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor, RestPagination, RestService } from '@app/shared/rest' | ||
5 | import { SortMeta } from 'primeng/api' | ||
6 | import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | ||
7 | import { concat, Observable } from 'rxjs' | ||
8 | import { environment } from '../../../environments/environment' | ||
9 | |||
10 | @Injectable() | ||
11 | export class RedundancyService { | ||
12 | static BASE_REDUNDANCY_URL = environment.apiUrl + '/api/v1/server/redundancy' | ||
13 | |||
14 | constructor ( | ||
15 | private authHttp: HttpClient, | ||
16 | private restService: RestService, | ||
17 | private restExtractor: RestExtractor | ||
18 | ) { } | ||
19 | |||
20 | updateRedundancy (host: string, redundancyAllowed: boolean) { | ||
21 | const url = RedundancyService.BASE_REDUNDANCY_URL + '/' + host | ||
22 | |||
23 | const body = { redundancyAllowed } | ||
24 | |||
25 | return this.authHttp.put(url, body) | ||
26 | .pipe( | ||
27 | map(this.restExtractor.extractDataBool), | ||
28 | catchError(err => this.restExtractor.handleError(err)) | ||
29 | ) | ||
30 | } | ||
31 | |||
32 | listVideoRedundancies (options: { | ||
33 | pagination: RestPagination, | ||
34 | sort: SortMeta, | ||
35 | target?: VideoRedundanciesTarget | ||
36 | }): Observable<ResultList<VideoRedundancy>> { | ||
37 | const { pagination, sort, target } = options | ||
38 | |||
39 | let params = new HttpParams() | ||
40 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
41 | |||
42 | if (target) params = params.append('target', target) | ||
43 | |||
44 | return this.authHttp.get<ResultList<VideoRedundancy>>(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { params }) | ||
45 | .pipe( | ||
46 | catchError(res => this.restExtractor.handleError(res)) | ||
47 | ) | ||
48 | } | ||
49 | |||
50 | addVideoRedundancy (video: Video) { | ||
51 | return this.authHttp.post(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { videoId: video.id }) | ||
52 | .pipe( | ||
53 | catchError(res => this.restExtractor.handleError(res)) | ||
54 | ) | ||
55 | } | ||
56 | |||
57 | removeVideoRedundancies (redundancy: VideoRedundancy) { | ||
58 | const observables = redundancy.redundancies.streamingPlaylists.map(r => r.id) | ||
59 | .concat(redundancy.redundancies.files.map(r => r.id)) | ||
60 | .map(id => this.removeRedundancy(id)) | ||
61 | |||
62 | return concat(...observables) | ||
63 | .pipe(toArray()) | ||
64 | } | ||
65 | |||
66 | private removeRedundancy (redundancyId: number) { | ||
67 | return this.authHttp.delete(RedundancyService.BASE_REDUNDANCY_URL + '/videos/' + redundancyId) | ||
68 | .pipe( | ||
69 | map(this.restExtractor.extractDataBool), | ||
70 | catchError(res => this.restExtractor.handleError(res)) | ||
71 | ) | ||
72 | } | ||
73 | } | ||
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.ts b/client/src/app/shared/video/video-actions-dropdown.component.ts index afdeab18d..390d74c52 100644 --- a/client/src/app/shared/video/video-actions-dropdown.component.ts +++ b/client/src/app/shared/video/video-actions-dropdown.component.ts | |||
@@ -14,6 +14,7 @@ import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklis | |||
14 | import { VideoBlacklistService } from '@app/shared/video-blacklist' | 14 | import { VideoBlacklistService } from '@app/shared/video-blacklist' |
15 | import { ScreenService } from '@app/shared/misc/screen.service' | 15 | import { ScreenService } from '@app/shared/misc/screen.service' |
16 | import { VideoCaption } from '@shared/models' | 16 | import { VideoCaption } from '@shared/models' |
17 | import { RedundancyService } from '@app/shared/video/redundancy.service' | ||
17 | 18 | ||
18 | export type VideoActionsDisplayType = { | 19 | export type VideoActionsDisplayType = { |
19 | playlist?: boolean | 20 | playlist?: boolean |
@@ -22,6 +23,7 @@ export type VideoActionsDisplayType = { | |||
22 | blacklist?: boolean | 23 | blacklist?: boolean |
23 | delete?: boolean | 24 | delete?: boolean |
24 | report?: boolean | 25 | report?: boolean |
26 | duplicate?: boolean | ||
25 | } | 27 | } |
26 | 28 | ||
27 | @Component({ | 29 | @Component({ |
@@ -46,7 +48,8 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
46 | update: true, | 48 | update: true, |
47 | blacklist: true, | 49 | blacklist: true, |
48 | delete: true, | 50 | delete: true, |
49 | report: true | 51 | report: true, |
52 | duplicate: true | ||
50 | } | 53 | } |
51 | @Input() placement = 'left' | 54 | @Input() placement = 'left' |
52 | 55 | ||
@@ -74,6 +77,7 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
74 | private screenService: ScreenService, | 77 | private screenService: ScreenService, |
75 | private videoService: VideoService, | 78 | private videoService: VideoService, |
76 | private blocklistService: BlocklistService, | 79 | private blocklistService: BlocklistService, |
80 | private redundancyService: RedundancyService, | ||
77 | private i18n: I18n | 81 | private i18n: I18n |
78 | ) { } | 82 | ) { } |
79 | 83 | ||
@@ -144,6 +148,10 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
144 | return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled | 148 | return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled |
145 | } | 149 | } |
146 | 150 | ||
151 | canVideoBeDuplicated () { | ||
152 | return this.video.canBeDuplicatedBy(this.user) | ||
153 | } | ||
154 | |||
147 | /* Action handlers */ | 155 | /* Action handlers */ |
148 | 156 | ||
149 | async unblacklistVideo () { | 157 | async unblacklistVideo () { |
@@ -186,6 +194,18 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
186 | ) | 194 | ) |
187 | } | 195 | } |
188 | 196 | ||
197 | duplicateVideo () { | ||
198 | this.redundancyService.addVideoRedundancy(this.video) | ||
199 | .subscribe( | ||
200 | () => { | ||
201 | const message = this.i18n('This video will be duplicated by your instance.') | ||
202 | this.notifier.success(message) | ||
203 | }, | ||
204 | |||
205 | err => this.notifier.error(err.message) | ||
206 | ) | ||
207 | } | ||
208 | |||
189 | onVideoBlacklisted () { | 209 | onVideoBlacklisted () { |
190 | this.videoBlacklisted.emit() | 210 | this.videoBlacklisted.emit() |
191 | } | 211 | } |
@@ -234,6 +254,12 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
234 | isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable() | 254 | isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable() |
235 | }, | 255 | }, |
236 | { | 256 | { |
257 | label: this.i18n('Duplicate (redundancy)'), | ||
258 | handler: () => this.duplicateVideo(), | ||
259 | isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.duplicate && this.canVideoBeDuplicated(), | ||
260 | iconName: 'cloud-download' | ||
261 | }, | ||
262 | { | ||
237 | label: this.i18n('Delete'), | 263 | label: this.i18n('Delete'), |
238 | handler: () => this.removeVideo(), | 264 | handler: () => this.removeVideo(), |
239 | isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(), | 265 | isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(), |
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html index 46c49c15b..819be6d48 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html | |||
@@ -50,7 +50,7 @@ | |||
50 | </div> | 50 | </div> |
51 | 51 | ||
52 | <div class="video-actions"> | 52 | <div class="video-actions"> |
53 | <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown --> | 53 | <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown: https://github.com/ng-bootstrap/ng-bootstrap/issues/3495 --> |
54 | <my-video-actions-dropdown | 54 | <my-video-actions-dropdown |
55 | *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left" | 55 | *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left" |
56 | (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()" | 56 | (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()" |
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts index 598a7a983..1dfb3eec7 100644 --- a/client/src/app/shared/video/video-miniature.component.ts +++ b/client/src/app/shared/video/video-miniature.component.ts | |||
@@ -64,7 +64,8 @@ export class VideoMiniatureComponent implements OnInit { | |||
64 | update: true, | 64 | update: true, |
65 | blacklist: true, | 65 | blacklist: true, |
66 | delete: true, | 66 | delete: true, |
67 | report: true | 67 | report: true, |
68 | duplicate: false | ||
68 | } | 69 | } |
69 | showActions = false | 70 | showActions = false |
70 | serverConfig: ServerConfig | 71 | serverConfig: ServerConfig |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index fb98d5382..546518cca 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -42,6 +42,9 @@ export class Video implements VideoServerModel { | |||
42 | dislikes: number | 42 | dislikes: number |
43 | nsfw: boolean | 43 | nsfw: boolean |
44 | 44 | ||
45 | originInstanceUrl: string | ||
46 | originInstanceHost: string | ||
47 | |||
45 | waitTranscoding?: boolean | 48 | waitTranscoding?: boolean |
46 | state?: VideoConstant<VideoState> | 49 | state?: VideoConstant<VideoState> |
47 | scheduledUpdate?: VideoScheduleUpdate | 50 | scheduledUpdate?: VideoScheduleUpdate |
@@ -86,22 +89,31 @@ export class Video implements VideoServerModel { | |||
86 | this.waitTranscoding = hash.waitTranscoding | 89 | this.waitTranscoding = hash.waitTranscoding |
87 | this.state = hash.state | 90 | this.state = hash.state |
88 | this.description = hash.description | 91 | this.description = hash.description |
92 | |||
89 | this.duration = hash.duration | 93 | this.duration = hash.duration |
90 | this.durationLabel = durationToString(hash.duration) | 94 | this.durationLabel = durationToString(hash.duration) |
95 | |||
91 | this.id = hash.id | 96 | this.id = hash.id |
92 | this.uuid = hash.uuid | 97 | this.uuid = hash.uuid |
98 | |||
93 | this.isLocal = hash.isLocal | 99 | this.isLocal = hash.isLocal |
94 | this.name = hash.name | 100 | this.name = hash.name |
101 | |||
95 | this.thumbnailPath = hash.thumbnailPath | 102 | this.thumbnailPath = hash.thumbnailPath |
96 | this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath | 103 | this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath |
104 | |||
97 | this.previewPath = hash.previewPath | 105 | this.previewPath = hash.previewPath |
98 | this.previewUrl = absoluteAPIUrl + hash.previewPath | 106 | this.previewUrl = absoluteAPIUrl + hash.previewPath |
107 | |||
99 | this.embedPath = hash.embedPath | 108 | this.embedPath = hash.embedPath |
100 | this.embedUrl = absoluteAPIUrl + hash.embedPath | 109 | this.embedUrl = absoluteAPIUrl + hash.embedPath |
110 | |||
101 | this.views = hash.views | 111 | this.views = hash.views |
102 | this.likes = hash.likes | 112 | this.likes = hash.likes |
103 | this.dislikes = hash.dislikes | 113 | this.dislikes = hash.dislikes |
114 | |||
104 | this.nsfw = hash.nsfw | 115 | this.nsfw = hash.nsfw |
116 | |||
105 | this.account = hash.account | 117 | this.account = hash.account |
106 | this.channel = hash.channel | 118 | this.channel = hash.channel |
107 | 119 | ||
@@ -124,6 +136,9 @@ export class Video implements VideoServerModel { | |||
124 | this.blacklistedReason = hash.blacklistedReason | 136 | this.blacklistedReason = hash.blacklistedReason |
125 | 137 | ||
126 | this.userHistory = hash.userHistory | 138 | this.userHistory = hash.userHistory |
139 | |||
140 | this.originInstanceHost = this.account.host | ||
141 | this.originInstanceUrl = 'https://' + this.originInstanceHost | ||
127 | } | 142 | } |
128 | 143 | ||
129 | isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { | 144 | isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { |
@@ -152,4 +167,8 @@ export class Video implements VideoServerModel { | |||
152 | isUpdatableBy (user: AuthUser) { | 167 | isUpdatableBy (user: AuthUser) { |
153 | return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) | 168 | return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) |
154 | } | 169 | } |
170 | |||
171 | canBeDuplicatedBy (user: AuthUser) { | ||
172 | return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) | ||
173 | } | ||
155 | } | 174 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index bc3a3ffdd..a382777f5 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html | |||
@@ -188,6 +188,11 @@ | |||
188 | <span class="video-attribute-value">{{ video.privacy.label }}</span> | 188 | <span class="video-attribute-value">{{ video.privacy.label }}</span> |
189 | </div> | 189 | </div> |
190 | 190 | ||
191 | <div *ngIf="video.isLocal === false" class="video-attribute"> | ||
192 | <span i18n class="video-attribute-label">Origin instance</span> | ||
193 | <a class="video-attribute-value" target="_blank" rel="noopener noreferrer" [href]="video.originInstanceUrl">{{ video.originInstanceHost }}</a> | ||
194 | </div> | ||
195 | |||
191 | <div *ngIf="!!video.originallyPublishedAt" class="video-attribute"> | 196 | <div *ngIf="!!video.originallyPublishedAt" class="video-attribute"> |
192 | <span i18n class="video-attribute-label">Originally published</span> | 197 | <span i18n class="video-attribute-label">Originally published</span> |
193 | <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span> | 198 | <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span> |