diff options
Diffstat (limited to 'client/src/app')
145 files changed, 1675 insertions, 415 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-instance/contact-admin-modal.component.ts b/client/src/app/+about/about-instance/contact-admin-modal.component.ts index 2ed41e741..d5e146b82 100644 --- a/client/src/app/+about/about-instance/contact-admin-modal.component.ts +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.ts | |||
@@ -51,7 +51,7 @@ export class ContactAdminModalComponent extends FormReactive implements OnInit { | |||
51 | } | 51 | } |
52 | 52 | ||
53 | show () { | 53 | show () { |
54 | this.openedModal = this.modalService.open(this.modal, { keyboard: false }) | 54 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) |
55 | } | 55 | } |
56 | 56 | ||
57 | hide () { | 57 | hide () { |
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.html b/client/src/app/+accounts/accounts.component.html index 85f7dd30c..6a76393b9 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -7,14 +7,13 @@ | |||
7 | <div class="actor-info"> | 7 | <div class="actor-info"> |
8 | <div class="actor-names"> | 8 | <div class="actor-names"> |
9 | <div class="actor-display-name">{{ account.displayName }}</div> | 9 | <div class="actor-display-name">{{ account.displayName }}</div> |
10 | <div class="actor-name">{{ account.nameWithHost }} | 10 | <div class="actor-name"> |
11 | 11 | <span>{{ account.nameWithHost }}</span> | |
12 | <button ngxClipboard [cbContent]="account.nameWithHostForced" (click)="activateCopiedMessage()" | 12 | <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" |
13 | class="btn btn-outline-secondary btn-sm copy-button" | 13 | class="btn btn-outline-secondary btn-sm copy-button" |
14 | > | 14 | > |
15 | <span class="glyphicon glyphicon-copy"></span> | 15 | <span class="glyphicon glyphicon-copy"></span> |
16 | </button> | 16 | </button> |
17 | |||
18 | </div> | 17 | </div> |
19 | <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> | 18 | <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> |
20 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> | 19 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> |
@@ -38,12 +37,12 @@ | |||
38 | </div> | 37 | </div> |
39 | </div> | 38 | </div> |
40 | 39 | ||
41 | <div class="links"> | 40 | <div class="links w-100"> |
42 | <a i18n routerLink="video-channels" routerLinkActive="active" class="title-page">Video channels</a> | 41 | <ng-template #linkTemplate let-item="item"> |
43 | 42 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | |
44 | <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a> | 43 | </ng-template> |
45 | 44 | ||
46 | <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a> | 45 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> |
47 | </div> | 46 | </div> |
48 | </div> | 47 | </div> |
49 | 48 | ||
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index ad611f221..4fea0e4ed 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -10,6 +10,7 @@ import { User, UserRight } from '../../../../shared' | |||
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 11 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
12 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 12 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
13 | import { ListOverflowItem } from '@app/shared/misc/list-overflow.component' | ||
13 | 14 | ||
14 | @Component({ | 15 | @Component({ |
15 | templateUrl: './accounts.component.html', | 16 | templateUrl: './accounts.component.html', |
@@ -19,6 +20,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
19 | account: Account | 20 | account: Account |
20 | accountUser: User | 21 | accountUser: User |
21 | videoChannels: VideoChannel[] = [] | 22 | videoChannels: VideoChannel[] = [] |
23 | links: ListOverflowItem[] = [] | ||
22 | 24 | ||
23 | isAccountManageable = false | 25 | isAccountManageable = false |
24 | accountFollowerTitle = '' | 26 | accountFollowerTitle = '' |
@@ -70,6 +72,12 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
70 | 72 | ||
71 | err => this.notifier.error(err.message) | 73 | err => this.notifier.error(err.message) |
72 | ) | 74 | ) |
75 | |||
76 | this.links = [ | ||
77 | { label: this.i18n('Video channels'), routerLink: 'video-channels' }, | ||
78 | { label: this.i18n('Videos'), routerLink: 'videos' }, | ||
79 | { label: this.i18n('About'), routerLink: 'about' } | ||
80 | ] | ||
73 | } | 81 | } |
74 | 82 | ||
75 | ngOnDestroy () { | 83 | ngOnDestroy () { |
@@ -96,7 +104,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
96 | } | 104 | } |
97 | 105 | ||
98 | subscribersDisplayFor (count: number) { | 106 | subscribersDisplayFor (count: number) { |
99 | return this.i18n(`{count, plural, =1 {1 subscriber} other {${count} subscribers}}`, { count }) | 107 | return this.i18n('{count, plural, =1 {1 subscriber} other {{{count}} subscribers}}', { count }) |
100 | } | 108 | } |
101 | 109 | ||
102 | private getUserIfNeeded (account: Account) { | 110 | private getUserIfNeeded (account: Account) { |
diff --git a/client/src/app/+admin/admin.component.html b/client/src/app/+admin/admin.component.html index 9a3d90c18..6c98fe453 100644 --- a/client/src/app/+admin/admin.component.html +++ b/client/src/app/+admin/admin.component.html | |||
@@ -1,28 +1,10 @@ | |||
1 | <div class="row"> | 1 | <div class="row"> |
2 | <div class="sub-menu"> | 2 | <div class="sub-menu"> |
3 | <a i18n *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active" class="title-page"> | 3 | <ng-template #linkTemplate let-item="item"> |
4 | Users | 4 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> |
5 | </a> | 5 | </ng-template> |
6 | 6 | ||
7 | <a i18n *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page"> | 7 | <list-overflow [items]="items" [itemTemplate]="linkTemplate"></list-overflow> |
8 | Manage follows | ||
9 | </a> | ||
10 | |||
11 | <a i18n *ngIf="hasVideoAbusesRight() || hasVideoBlacklistRight()" routerLink="/admin/moderation" routerLinkActive="active" class="title-page"> | ||
12 | Moderation | ||
13 | </a> | ||
14 | |||
15 | <a i18n *ngIf="hasConfigRight()" routerLink="/admin/config" routerLinkActive="active" class="title-page"> | ||
16 | Configuration | ||
17 | </a> | ||
18 | |||
19 | <a i18n *ngIf="hasPluginsRight()" routerLink="/admin/plugins" routerLinkActive="active" class="title-page"> | ||
20 | Plugins/Themes | ||
21 | </a> | ||
22 | |||
23 | <a i18n *ngIf="hasJobsRight() || hasLogsRight() || hasDebugRight()" routerLink="/admin/system" routerLinkActive="active" class="title-page"> | ||
24 | System | ||
25 | </a> | ||
26 | </div> | 8 | </div> |
27 | 9 | ||
28 | <div class="margin-content"> | 10 | <div class="margin-content"> |
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index b23999d40..9662522dc 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts | |||
@@ -1,12 +1,28 @@ | |||
1 | import { Component } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { UserRight } from '../../../../shared' | 2 | import { UserRight } from '../../../../shared' |
3 | import { AuthService } from '../core/auth/auth.service' | 3 | import { AuthService } from '../core/auth/auth.service' |
4 | import { ListOverflowItem } from '@app/shared/misc/list-overflow.component' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | 6 | ||
5 | @Component({ | 7 | @Component({ |
6 | templateUrl: './admin.component.html' | 8 | templateUrl: './admin.component.html' |
7 | }) | 9 | }) |
8 | export class AdminComponent { | 10 | export class AdminComponent implements OnInit { |
9 | constructor (private auth: AuthService) {} | 11 | items: ListOverflowItem[] = [] |
12 | |||
13 | constructor ( | ||
14 | private auth: AuthService, | ||
15 | private i18n: I18n | ||
16 | ) {} | ||
17 | |||
18 | ngOnInit () { | ||
19 | if (this.hasUsersRight()) this.items.push({ label: this.i18n('Users'), routerLink: '/admin/users' }) | ||
20 | if (this.hasServerFollowRight()) this.items.push({ label: this.i18n('Follows & redundancies'), routerLink: '/admin/follows' }) | ||
21 | if (this.hasVideoAbusesRight() || this.hasVideoBlacklistRight()) this.items.push({ label: this.i18n('Moderation'), routerLink: '/admin/moderation' }) | ||
22 | if (this.hasConfigRight()) this.items.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' }) | ||
23 | if (this.hasPluginsRight()) this.items.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' }) | ||
24 | if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) this.items.push({ label: this.i18n('System'), routerLink: '/admin/system' }) | ||
25 | } | ||
10 | 26 | ||
11 | hasUsersRight () { | 27 | hasUsersRight () { |
12 | return this.auth.getUser().hasRight(UserRight.MANAGE_USERS) | 28 | return this.auth.getUser().hasRight(UserRight.MANAGE_USERS) |
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..b09614061 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 | |||
@@ -41,7 +41,7 @@ | |||
41 | 41 | ||
42 | <div> | 42 | <div> |
43 | <p-multiSelect | 43 | <p-multiSelect |
44 | inputId="instanceCategories" [options]="categoryItems" formControlName="categories" showToggleAll="false" | 44 | inputId="instanceCategories" [options]="categoryItems" formControlName="categories" [showToggleAll]="false" |
45 | [defaultLabel]="getDefaultCategoryLabel()" [selectedItemsLabel]="getSelectedCategoryLabel()" | 45 | [defaultLabel]="getDefaultCategoryLabel()" [selectedItemsLabel]="getSelectedCategoryLabel()" |
46 | emptyFilterMessage="No results found" i18n-emptyFilterMessage | 46 | emptyFilterMessage="No results found" i18n-emptyFilterMessage |
47 | ></p-multiSelect> | 47 | ></p-multiSelect> |
@@ -53,7 +53,7 @@ | |||
53 | 53 | ||
54 | <div> | 54 | <div> |
55 | <p-multiSelect | 55 | <p-multiSelect |
56 | inputId="instanceLanguages" [options]="languageItems" formControlName="languages" showToggleAll="false" | 56 | inputId="instanceLanguages" [options]="languageItems" formControlName="languages" [showToggleAll]="false" |
57 | [defaultLabel]="getDefaultLanguageLabel()" [selectedItemsLabel]="getSelectedLanguageLabel()" | 57 | [defaultLabel]="getDefaultLanguageLabel()" [selectedItemsLabel]="getSelectedLanguageLabel()" |
58 | emptyFilterMessage="No results found" i18n-emptyFilterMessage | 58 | emptyFilterMessage="No results found" i18n-emptyFilterMessage |
59 | ></p-multiSelect> | 59 | ></p-multiSelect> |
@@ -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/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html index 6b5f3b450..c532b5f32 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.html +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | <div class="caption"> | 6 | <div class="caption"> |
7 | <input | 7 | <input |
8 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 8 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." |
9 | (keyup)="onSearch($event.target.value)" | 9 | (keyup)="onSearch($event)" |
10 | > | 10 | > |
11 | </div> | 11 | </div> |
12 | </ng-template> | 12 | </ng-template> |
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/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html index 5a252eda9..01aba0c11 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.html +++ b/client/src/app/+admin/follows/following-list/following-list.component.html | |||
@@ -7,7 +7,7 @@ | |||
7 | <div> | 7 | <div> |
8 | <input | 8 | <input |
9 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 9 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." |
10 | (keyup)="onSearch($event.target.value)" | 10 | (keyup)="onSearch($event)" |
11 | > | 11 | > |
12 | </div> | 12 | </div> |
13 | </div> | 13 | </div> |
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/moderation/instance-blocklist/instance-account-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts index 032bf745a..03e3379e6 100644 --- a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts | |||
@@ -2,7 +2,7 @@ import { Component, OnInit } 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 { RestPagination, RestTable } from '@app/shared' | 4 | import { RestPagination, RestTable } from '@app/shared' |
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | 5 | import { SortMeta } from 'primeng/api' |
6 | import { AccountBlock, BlocklistService } from '@app/shared/blocklist' | 6 | import { AccountBlock, BlocklistService } from '@app/shared/blocklist' |
7 | 7 | ||
8 | @Component({ | 8 | @Component({ |
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts index db3dfcd1c..d92a4f195 100644 --- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts | |||
@@ -2,7 +2,7 @@ import { Component, OnInit } 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 { RestPagination, RestTable } from '@app/shared' | 4 | import { RestPagination, RestTable } from '@app/shared' |
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | 5 | import { SortMeta } from 'primeng/api' |
6 | import { BlocklistService } from '@app/shared/blocklist' | 6 | import { BlocklistService } from '@app/shared/blocklist' |
7 | import { ServerBlock } from '../../../../../../shared' | 7 | import { ServerBlock } from '../../../../../../shared' |
8 | 8 | ||
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts index f8a5ef8cb..29f90194b 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts | |||
@@ -38,7 +38,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
38 | 38 | ||
39 | openModal (abuseToComment: VideoAbuse) { | 39 | openModal (abuseToComment: VideoAbuse) { |
40 | this.abuseToComment = abuseToComment | 40 | this.abuseToComment = abuseToComment |
41 | this.openedModal = this.modalService.open(this.modal) | 41 | this.openedModal = this.modalService.open(this.modal, { centered: true }) |
42 | 42 | ||
43 | this.form.patchValue({ | 43 | this.form.patchValue({ |
44 | moderationComment: this.abuseToComment.moderationComment | 44 | moderationComment: this.abuseToComment.moderationComment |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts index 778f18d3d..51114e087 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { Account } from '../../../shared/account/account.model' | 2 | import { Account } from '../../../shared/account/account.model' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { SortMeta } from 'primeng/components/common/sortmeta' | 4 | import { SortMeta } from 'primeng/api' |
5 | import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' | 5 | import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' |
6 | import { RestPagination, RestTable, VideoAbuseService } from '../../../shared' | 6 | import { RestPagination, RestTable, VideoAbuseService } from '../../../shared' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts index 5876f658b..a215714b8 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { SortMeta } from 'primeng/components/common/sortmeta' | 2 | import { SortMeta } from 'primeng/api' |
3 | import { Notifier, ServerService } from '@app/core' | 3 | import { Notifier, ServerService } from '@app/core' |
4 | import { ConfirmService } from '../../../core' | 4 | import { ConfirmService } from '../../../core' |
5 | import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' | 5 | import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' |
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html index 6ec6301b1..5f18028c9 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | </div> | 3 | </div> |
4 | 4 | ||
5 | <div class="search-bar"> | 5 | <div class="search-bar"> |
6 | <input type="text" (input)="onSearchChange($event.target.value)" i18n-placeholder placeholder="Search..."/> | 6 | <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..."/> |
7 | </div> | 7 | </div> |
8 | 8 | ||
9 | <div class="alert alert-info" i18n *ngIf="pluginInstalled"> | 9 | <div class="alert alert-info" i18n *ngIf="pluginInstalled"> |
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts index e08ded3f1..dc59e759b 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts | |||
@@ -70,8 +70,10 @@ export class PluginSearchComponent implements OnInit { | |||
70 | this.reloadPlugins() | 70 | this.reloadPlugins() |
71 | } | 71 | } |
72 | 72 | ||
73 | onSearchChange (search: string) { | 73 | onSearchChange (event: Event) { |
74 | this.searchSubject.next(search) | 74 | const target = event.target as HTMLInputElement |
75 | |||
76 | this.searchSubject.next(target.value) | ||
75 | } | 77 | } |
76 | 78 | ||
77 | reloadPlugins () { | 79 | reloadPlugins () { |
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/+admin/users/user-edit/user-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts index e726ec4d7..1769c0de0 100644 --- a/client/src/app/+admin/users/user-edit/user-create.component.ts +++ b/client/src/app/+admin/users/user-edit/user-create.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router, ActivatedRoute } from '@angular/router' |
3 | import { AuthService, Notifier, ServerService } from '@app/core' | 3 | import { AuthService, Notifier, ServerService } from '@app/core' |
4 | import { UserCreate, UserRole } from '../../../../../../shared' | 4 | import { UserCreate, UserRole } from '../../../../../../shared' |
5 | import { UserEdit } from './user-edit' | 5 | import { UserEdit } from './user-edit' |
@@ -23,6 +23,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
23 | protected configService: ConfigService, | 23 | protected configService: ConfigService, |
24 | protected auth: AuthService, | 24 | protected auth: AuthService, |
25 | private userValidatorsService: UserValidatorsService, | 25 | private userValidatorsService: UserValidatorsService, |
26 | private route: ActivatedRoute, | ||
26 | private router: Router, | 27 | private router: Router, |
27 | private notifier: Notifier, | 28 | private notifier: Notifier, |
28 | private userService: UserService, | 29 | private userService: UserService, |
@@ -45,7 +46,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
45 | this.buildForm({ | 46 | this.buildForm({ |
46 | username: this.userValidatorsService.USER_USERNAME, | 47 | username: this.userValidatorsService.USER_USERNAME, |
47 | email: this.userValidatorsService.USER_EMAIL, | 48 | email: this.userValidatorsService.USER_EMAIL, |
48 | password: this.userValidatorsService.USER_PASSWORD, | 49 | password: this.isPasswordOptional() ? this.userValidatorsService.USER_PASSWORD_OPTIONAL : this.userValidatorsService.USER_PASSWORD, |
49 | role: this.userValidatorsService.USER_ROLE, | 50 | role: this.userValidatorsService.USER_ROLE, |
50 | videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, | 51 | videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, |
51 | videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, | 52 | videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, |
@@ -78,6 +79,11 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
78 | return true | 79 | return true |
79 | } | 80 | } |
80 | 81 | ||
82 | isPasswordOptional () { | ||
83 | const serverConfig = this.route.snapshot.data.serverConfig | ||
84 | return serverConfig.email.enabled | ||
85 | } | ||
86 | |||
81 | getFormButtonTitle () { | 87 | getFormButtonTitle () { |
82 | return this.i18n('Create user') | 88 | return this.i18n('Create user') |
83 | } | 89 | } |
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 4ff4d0d12..2aca5ddca 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -29,6 +29,13 @@ | |||
29 | 29 | ||
30 | <div class="form-group" *ngIf="isCreation()"> | 30 | <div class="form-group" *ngIf="isCreation()"> |
31 | <label i18n for="password">Password</label> | 31 | <label i18n for="password">Password</label> |
32 | <my-help *ngIf="isPasswordOptional()"> | ||
33 | <ng-template ptTemplate="customHtml"> | ||
34 | <ng-container i18n> | ||
35 | If you leave the password empty, an email will be sent to the user. | ||
36 | </ng-container> | ||
37 | </ng-template> | ||
38 | </my-help> | ||
32 | <input | 39 | <input |
33 | type="password" id="password" autocomplete="new-password" | 40 | type="password" id="password" autocomplete="new-password" |
34 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | 41 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" |
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index d1682a99d..1ab2e9dbf 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts | |||
@@ -92,6 +92,10 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
92 | return false | 92 | return false |
93 | } | 93 | } |
94 | 94 | ||
95 | isPasswordOptional () { | ||
96 | return false | ||
97 | } | ||
98 | |||
95 | getFormButtonTitle () { | 99 | getFormButtonTitle () { |
96 | return this.i18n('Update user') | 100 | return this.i18n('Update user') |
97 | } | 101 | } |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index ca05bac19..249883efc 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -25,7 +25,7 @@ | |||
25 | <div> | 25 | <div> |
26 | <input | 26 | <input |
27 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 27 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." |
28 | (keyup)="onSearch($event.target.value)" | 28 | (keyup)="onSearch($event)" |
29 | > | 29 | > |
30 | </div> | 30 | </div> |
31 | </div> | 31 | </div> |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index 7c5e8eaa4..6e9a1feda 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { AuthService, Notifier } from '@app/core' | 2 | import { AuthService, Notifier } from '@app/core' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/api' |
4 | import { ConfirmService, ServerService } from '../../../core' | 4 | import { ConfirmService, ServerService } from '../../../core' |
5 | import { RestPagination, RestTable, UserService } from '../../../shared' | 5 | import { RestPagination, RestTable, UserService } from '../../../shared' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
diff --git a/client/src/app/+admin/users/users.routes.ts b/client/src/app/+admin/users/users.routes.ts index 8b3791bd3..2d4f9305e 100644 --- a/client/src/app/+admin/users/users.routes.ts +++ b/client/src/app/+admin/users/users.routes.ts | |||
@@ -5,6 +5,7 @@ import { UserRight } from '../../../../../shared' | |||
5 | import { UsersComponent } from './users.component' | 5 | import { UsersComponent } from './users.component' |
6 | import { UserCreateComponent, UserUpdateComponent } from './user-edit' | 6 | import { UserCreateComponent, UserUpdateComponent } from './user-edit' |
7 | import { UserListComponent } from './user-list' | 7 | import { UserListComponent } from './user-list' |
8 | import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' | ||
8 | 9 | ||
9 | export const UsersRoutes: Routes = [ | 10 | export const UsersRoutes: Routes = [ |
10 | { | 11 | { |
@@ -36,6 +37,9 @@ export const UsersRoutes: Routes = [ | |||
36 | meta: { | 37 | meta: { |
37 | title: 'Create a user' | 38 | title: 'Create a user' |
38 | } | 39 | } |
40 | }, | ||
41 | resolve: { | ||
42 | serverConfig: ServerConfigResolver | ||
39 | } | 43 | } |
40 | }, | 44 | }, |
41 | { | 45 | { |
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts index e3025dec4..15b58e45c 100644 --- a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts | |||
@@ -2,7 +2,7 @@ import { Component, OnInit } 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 { RestPagination, RestTable } from '@app/shared' | 4 | import { RestPagination, RestTable } from '@app/shared' |
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | 5 | import { SortMeta } from 'primeng/api' |
6 | import { AccountBlock, BlocklistService } from '@app/shared/blocklist' | 6 | import { AccountBlock, BlocklistService } from '@app/shared/blocklist' |
7 | 7 | ||
8 | @Component({ | 8 | @Component({ |
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts index 4c5cc28b8..e735d4ab7 100644 --- a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts | |||
@@ -2,7 +2,7 @@ import { Component, OnInit } 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 { RestPagination, RestTable } from '@app/shared' | 4 | import { RestPagination, RestTable } from '@app/shared' |
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | 5 | import { SortMeta } from 'primeng/api' |
6 | import { ServerBlock } from '../../../../../shared' | 6 | import { ServerBlock } from '../../../../../shared' |
7 | import { BlocklistService } from '@app/shared/blocklist' | 7 | import { BlocklistService } from '@app/shared/blocklist' |
8 | 8 | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts index 6df929ec9..d5682914e 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts +++ b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts | |||
@@ -53,7 +53,7 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O | |||
53 | show (videoChangeOwnership: VideoChangeOwnership) { | 53 | show (videoChangeOwnership: VideoChangeOwnership) { |
54 | this.videoChangeOwnership = videoChangeOwnership | 54 | this.videoChangeOwnership = videoChangeOwnership |
55 | this.modalService | 55 | this.modalService |
56 | .open(this.modal) | 56 | .open(this.modal, { centered: true }) |
57 | .result | 57 | .result |
58 | .then(() => this.acceptOwnership()) | 58 | .then(() => this.acceptOwnership()) |
59 | .catch(() => this.videoChangeOwnership = undefined) | 59 | .catch(() => this.videoChangeOwnership = undefined) |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts index aeeb0e5a7..cd8065800 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { RestPagination, RestTable } from '@app/shared' | 3 | import { RestPagination, RestTable } from '@app/shared' |
4 | import { SortMeta } from 'primeng/components/common/sortmeta' | 4 | import { SortMeta } from 'primeng/api' |
5 | import { VideoChangeOwnership } from '../../../../../shared' | 5 | import { VideoChangeOwnership } from '../../../../../shared' |
6 | import { VideoOwnershipService } from '@app/shared/video-ownership' | 6 | import { VideoOwnershipService } from '@app/shared/video-ownership' |
7 | import { Account } from '@app/shared/account/account.model' | 7 | import { Account } from '@app/shared/account/account.model' |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html index 17d8cde06..51a672734 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html | |||
@@ -28,7 +28,7 @@ | |||
28 | 28 | ||
29 | <div> | 29 | <div> |
30 | <p-multiSelect | 30 | <p-multiSelect |
31 | inputId="videoLanguages" [options]="languageItems" formControlName="videoLanguages" showToggleAll="true" | 31 | inputId="videoLanguages" [options]="languageItems" formControlName="videoLanguages" [showToggleAll]="true" |
32 | [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()" | 32 | [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()" |
33 | emptyFilterMessage="No results found" i18n-emptyFilterMessage | 33 | emptyFilterMessage="No results found" i18n-emptyFilterMessage |
34 | ></p-multiSelect> | 34 | ></p-multiSelect> |
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/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss index 20582e478..db0c7f94f 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss | |||
@@ -58,7 +58,7 @@ | |||
58 | margin: 20px 0 50px; | 58 | margin: 20px 0 50px; |
59 | } | 59 | } |
60 | 60 | ||
61 | @media screen and (max-width: 800px) { | 61 | @media screen and (max-width: $small-view) { |
62 | .video-channels-header { | 62 | .video-channels-header { |
63 | text-align: center; | 63 | text-align: center; |
64 | } | 64 | } |
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts index 21a10c8ff..74c42bb4f 100644 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { RestPagination, RestTable } from '@app/shared' | 2 | import { RestPagination, RestTable } from '@app/shared' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/api' |
4 | import { Notifier } from '@app/core' | 4 | import { Notifier } from '@app/core' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { VideoImport, VideoImportState } from '../../../../../shared/models/videos' | 6 | import { VideoImport, VideoImportState } from '../../../../../shared/models/videos' |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss index 4e4156b22..aed3302ba 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss | |||
@@ -43,7 +43,7 @@ | |||
43 | } | 43 | } |
44 | } | 44 | } |
45 | 45 | ||
46 | @media screen and (max-width: 800px) { | 46 | @media screen and (max-width: $small-view) { |
47 | .video-playlists-header { | 47 | .video-playlists-header { |
48 | text-align: center; | 48 | text-align: center; |
49 | } | 49 | } |
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts index 36d1ea091..f4e2b5955 100644 --- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts +++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts | |||
@@ -43,7 +43,7 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni | |||
43 | show (video: Video) { | 43 | show (video: Video) { |
44 | this.video = video | 44 | this.video = video |
45 | this.modalService | 45 | this.modalService |
46 | .open(this.modal) | 46 | .open(this.modal, { centered: true }) |
47 | .result | 47 | .result |
48 | .then(() => this.changeOwnership()) | 48 | .then(() => this.changeOwnership()) |
49 | .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing | 49 | .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing |
diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.ts b/client/src/app/+my-account/shared/actor-avatar-info.component.ts index 101dfa556..8e034bb82 100644 --- a/client/src/app/+my-account/shared/actor-avatar-info.component.ts +++ b/client/src/app/+my-account/shared/actor-avatar-info.component.ts | |||
@@ -11,7 +11,7 @@ import { ServerConfig } from '@shared/models' | |||
11 | styleUrls: [ './actor-avatar-info.component.scss' ] | 11 | styleUrls: [ './actor-avatar-info.component.scss' ] |
12 | }) | 12 | }) |
13 | export class ActorAvatarInfoComponent implements OnInit { | 13 | export class ActorAvatarInfoComponent implements OnInit { |
14 | @ViewChild('avatarfileInput', { static: false }) avatarfileInput: ElementRef<HTMLInputElement> | 14 | @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> |
15 | 15 | ||
16 | @Input() actor: VideoChannel | Account | 16 | @Input() actor: VideoChannel | Account |
17 | 17 | ||
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/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss index 2f62dd59d..e135b5cb4 100644 --- a/client/src/app/+signup/+register/register.component.scss +++ b/client/src/app/+signup/+register/register.component.scss | |||
@@ -44,7 +44,7 @@ | |||
44 | } | 44 | } |
45 | } | 45 | } |
46 | 46 | ||
47 | @media screen and (max-width: 500px) { | 47 | @media screen and (max-width: $mobile-view) { |
48 | width: auto; | 48 | width: auto; |
49 | } | 49 | } |
50 | } | 50 | } |
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/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index f0bb083ca..1087de113 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html | |||
@@ -7,32 +7,38 @@ | |||
7 | <div class="actor-info"> | 7 | <div class="actor-info"> |
8 | <div class="actor-names"> | 8 | <div class="actor-names"> |
9 | <div class="actor-display-name">{{ videoChannel.displayName }}</div> | 9 | <div class="actor-display-name">{{ videoChannel.displayName }}</div> |
10 | <div class="actor-name">{{ videoChannel.nameWithHost }} | 10 | <div class="actor-name"> |
11 | <button ngxClipboard [cbContent]="videoChannel.nameWithHost" (click)="activateCopiedMessage()" | 11 | <span>{{ videoChannel.nameWithHost }}</span> |
12 | <button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()" | ||
12 | class="btn btn-outline-secondary btn-sm copy-button" | 13 | class="btn btn-outline-secondary btn-sm copy-button" |
13 | > | 14 | > |
14 | <span class="glyphicon glyphicon-copy"></span> | 15 | <span class="glyphicon glyphicon-copy"></span> |
15 | </button> | 16 | </button> |
16 | </div> | 17 | </div> |
18 | </div> | ||
17 | 19 | ||
18 | <div class="right-buttons"> | 20 | <div class="right-buttons"> |
19 | <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a> | 21 | <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a> |
20 | <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> | 22 | <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> |
21 | </div> | ||
22 | </div> | 23 | </div> |
23 | <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | ||
24 | 24 | ||
25 | <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> | 25 | <div class="actor-lower"> |
26 | <span i18n>Created by {{ videoChannel.ownerBy }}</span> | 26 | <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> |
27 | <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | 27 | |
28 | </a> | 28 | <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> |
29 | <span i18n>Created by {{ videoChannel.ownerBy }}</span> | ||
30 | <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | ||
31 | </a> | ||
32 | </div> | ||
29 | </div> | 33 | </div> |
30 | </div> | 34 | </div> |
31 | 35 | ||
32 | <div class="links"> | 36 | <div class="links w-100"> |
33 | <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a> | 37 | <ng-template #linkTemplate let-item="item"> |
34 | <a i18n routerLink="video-playlists" routerLinkActive="active" class="title-page">Video playlists</a> | 38 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> |
35 | <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a> | 39 | </ng-template> |
40 | |||
41 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | ||
36 | </div> | 42 | </div> |
37 | </div> | 43 | </div> |
38 | 44 | ||
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index 50b69e7ac..6470629f8 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss | |||
@@ -8,6 +8,21 @@ | |||
8 | width: 100%; | 8 | width: 100%; |
9 | } | 9 | } |
10 | 10 | ||
11 | .actor-info { | ||
12 | display: grid !important; | ||
13 | grid-template-columns: 1fr auto; | ||
14 | grid-template-rows: 1fr auto / 1fr auto; | ||
15 | grid-template-areas: "name buttons" "lower buttons"; | ||
16 | |||
17 | @media screen and (max-width: #{map-get($grid-breakpoints, lg)}) { | ||
18 | grid-template-areas: "name name" "lower buttons"; | ||
19 | } | ||
20 | } | ||
21 | |||
22 | .actor-names { | ||
23 | grid-area: name; | ||
24 | } | ||
25 | |||
11 | .actor-name { | 26 | .actor-name { |
12 | flex-grow: 1; | 27 | flex-grow: 1; |
13 | 28 | ||
@@ -25,6 +40,9 @@ | |||
25 | margin-left: auto; | 40 | margin-left: auto; |
26 | margin-top: 20px; | 41 | margin-top: 20px; |
27 | 42 | ||
43 | grid-row: buttons-start / span buttons-end; | ||
44 | grid-column: buttons-start; | ||
45 | |||
28 | a { | 46 | a { |
29 | @include peertube-button-outline; | 47 | @include peertube-button-outline; |
30 | line-height: 1.8; | 48 | line-height: 1.8; |
@@ -33,4 +51,4 @@ | |||
33 | my-subscribe-button { | 51 | my-subscribe-button { |
34 | height: min-content; | 52 | height: min-content; |
35 | } | 53 | } |
36 | } \ No newline at end of file | 54 | } |
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts index 7b335b13f..0889ca854 100644 --- a/client/src/app/+video-channels/video-channels.component.ts +++ b/client/src/app/+video-channels/video-channels.component.ts | |||
@@ -9,16 +9,18 @@ import { AuthService, Notifier } from '@app/core' | |||
9 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 9 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
10 | import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' | 10 | import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
12 | import { ListOverflowItem } from '@app/shared/misc/list-overflow.component' | ||
12 | 13 | ||
13 | @Component({ | 14 | @Component({ |
14 | templateUrl: './video-channels.component.html', | 15 | templateUrl: './video-channels.component.html', |
15 | styleUrls: [ './video-channels.component.scss' ] | 16 | styleUrls: [ './video-channels.component.scss' ] |
16 | }) | 17 | }) |
17 | export class VideoChannelsComponent implements OnInit, OnDestroy { | 18 | export class VideoChannelsComponent implements OnInit, OnDestroy { |
18 | @ViewChild('subscribeButton', { static: false }) subscribeButton: SubscribeButtonComponent | 19 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
19 | 20 | ||
20 | videoChannel: VideoChannel | 21 | videoChannel: VideoChannel |
21 | hotkeys: Hotkey[] | 22 | hotkeys: Hotkey[] |
23 | links: ListOverflowItem[] = [] | ||
22 | isChannelManageable = false | 24 | isChannelManageable = false |
23 | 25 | ||
24 | private routeSub: Subscription | 26 | private routeSub: Subscription |
@@ -62,6 +64,12 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
62 | }, undefined, this.i18n('Subscribe to the account')) | 64 | }, undefined, this.i18n('Subscribe to the account')) |
63 | ] | 65 | ] |
64 | if (this.isUserLoggedIn()) this.hotkeysService.add(this.hotkeys) | 66 | if (this.isUserLoggedIn()) this.hotkeysService.add(this.hotkeys) |
67 | |||
68 | this.links = [ | ||
69 | { label: this.i18n('Videos'), routerLink: 'videos' }, | ||
70 | { label: this.i18n('Video playlists'), routerLink: 'video-playlists' }, | ||
71 | { label: this.i18n('About'), routerLink: 'about' } | ||
72 | ] | ||
65 | } | 73 | } |
66 | 74 | ||
67 | ngOnDestroy () { | 75 | ngOnDestroy () { |
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 2660c5377..f5a8dbd34 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html | |||
@@ -15,7 +15,7 @@ | |||
15 | </div> | 15 | </div> |
16 | 16 | ||
17 | <div class="header-right" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }"> | 17 | <div class="header-right" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }"> |
18 | <my-header></my-header> | 18 | <my-header class="w-100 d-flex justify-content-end"></my-header> |
19 | </div> | 19 | </div> |
20 | </div> | 20 | </div> |
21 | 21 | ||
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index 51a7a3dd1..a7be8e1af 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss | |||
@@ -16,12 +16,12 @@ | |||
16 | top: 0; | 16 | top: 0; |
17 | width: 100%; | 17 | width: 100%; |
18 | background-color: var(--mainBackgroundColor); | 18 | background-color: var(--mainBackgroundColor); |
19 | z-index: 1000; | 19 | z-index: z(header); |
20 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16); | 20 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16); |
21 | display: flex; | 21 | display: flex; |
22 | 22 | ||
23 | .top-left-block { | 23 | .top-left-block { |
24 | z-index: 1001; | 24 | z-index: z(headerLeft); |
25 | height: $header-height; | 25 | height: $header-height; |
26 | display: flex; | 26 | display: flex; |
27 | align-items: center; | 27 | align-items: center; |
@@ -61,7 +61,7 @@ | |||
61 | } | 61 | } |
62 | } | 62 | } |
63 | 63 | ||
64 | @media screen and (max-width: 500px) { | 64 | @media screen and (max-width: $mobile-view) { |
65 | width: 70px; | 65 | width: 70px; |
66 | 66 | ||
67 | .peertube-title { | 67 | .peertube-title { |
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 03eb83cb8..59966243b 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts | |||
@@ -25,8 +25,8 @@ import { InstanceService } from '@app/shared/instance/instance.service' | |||
25 | styleUrls: [ './app.component.scss' ] | 25 | styleUrls: [ './app.component.scss' ] |
26 | }) | 26 | }) |
27 | export class AppComponent implements OnInit { | 27 | export class AppComponent implements OnInit { |
28 | @ViewChild('welcomeModal', { static: false }) welcomeModal: WelcomeModalComponent | 28 | @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent |
29 | @ViewChild('instanceConfigWarningModal', { static: false }) instanceConfigWarningModal: InstanceConfigWarningModalComponent | 29 | @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent |
30 | 30 | ||
31 | isMenuDisplayed = true | 31 | isMenuDisplayed = true |
32 | isMenuChangedByUser = false | 32 | isMenuChangedByUser = false |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index dda705811..9e220a383 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -4,22 +4,21 @@ import { ServerService } from '@app/core' | |||
4 | import { ResetPasswordModule } from '@app/reset-password' | 4 | import { ResetPasswordModule } from '@app/reset-password' |
5 | 5 | ||
6 | import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core' | 6 | import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core' |
7 | import { ClipboardModule } from 'ngx-clipboard' | ||
8 | import 'focus-visible' | 7 | import 'focus-visible' |
9 | 8 | ||
10 | import { AppRoutingModule } from './app-routing.module' | 9 | import { AppRoutingModule } from './app-routing.module' |
11 | import { AppComponent } from './app.component' | 10 | import { AppComponent } from './app.component' |
12 | import { CoreModule } from './core' | 11 | import { CoreModule } from './core' |
13 | import { HeaderComponent } from './header' | 12 | import { HeaderComponent, SearchTypeaheadComponent, SuggestionsComponent, SuggestionComponent } from './header' |
14 | import { LoginModule } from './login' | 13 | import { LoginModule } from './login' |
15 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' | 14 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' |
16 | import { SharedModule } from './shared' | 15 | import { SharedModule } from './shared' |
17 | import { VideosModule } from './videos' | 16 | import { VideosModule } from './videos' |
18 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' | ||
19 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | ||
20 | import { SearchModule } from '@app/search' | 17 | import { SearchModule } from '@app/search' |
21 | import { WelcomeModalComponent } from '@app/modal/welcome-modal.component' | 18 | import { WelcomeModalComponent } from '@app/modal/welcome-modal.component' |
22 | import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component' | 19 | import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component' |
20 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '@shared/models' | ||
21 | import { APP_BASE_HREF } from '@angular/common' | ||
23 | 22 | ||
24 | export function metaFactory (serverService: ServerService): MetaLoader { | 23 | export function metaFactory (serverService: ServerService): MetaLoader { |
25 | return new MetaStaticLoader({ | 24 | return new MetaStaticLoader({ |
@@ -42,14 +41,15 @@ export function metaFactory (serverService: ServerService): MetaLoader { | |||
42 | LanguageChooserComponent, | 41 | LanguageChooserComponent, |
43 | AvatarNotificationComponent, | 42 | AvatarNotificationComponent, |
44 | HeaderComponent, | 43 | HeaderComponent, |
44 | SearchTypeaheadComponent, | ||
45 | SuggestionsComponent, | ||
46 | SuggestionComponent, | ||
45 | 47 | ||
46 | WelcomeModalComponent, | 48 | WelcomeModalComponent, |
47 | InstanceConfigWarningModalComponent | 49 | InstanceConfigWarningModalComponent |
48 | ], | 50 | ], |
49 | imports: [ | 51 | imports: [ |
50 | BrowserModule, | 52 | BrowserModule, |
51 | // FIXME: https://github.com/maxisam/ngx-clipboard/issues/133 | ||
52 | ClipboardModule, | ||
53 | 53 | ||
54 | CoreModule, | 54 | CoreModule, |
55 | SharedModule, | 55 | SharedModule, |
@@ -69,17 +69,17 @@ export function metaFactory (serverService: ServerService): MetaLoader { | |||
69 | 69 | ||
70 | AppRoutingModule // Put it after all the module because it has the 404 route | 70 | AppRoutingModule // Put it after all the module because it has the 404 route |
71 | ], | 71 | ], |
72 | |||
72 | providers: [ | 73 | providers: [ |
73 | { | 74 | { |
75 | provide: APP_BASE_HREF, | ||
76 | useValue: '/' | ||
77 | }, | ||
78 | |||
79 | { | ||
74 | provide: TRANSLATIONS, | 80 | provide: TRANSLATIONS, |
75 | useFactory: (locale: string) => { | 81 | useFactory: (locale: string) => { |
76 | // On dev mode, test localization | 82 | // Default locale, nothing to translate |
77 | if (isOnDevLocale()) { | ||
78 | locale = buildFileLocale(getDevLocale()) | ||
79 | return require(`raw-loader!../locale/angular.${locale}.xlf`) | ||
80 | } | ||
81 | |||
82 | // Default locale, nothing to translate | ||
83 | const completeLocale = getCompleteLocale(locale) | 83 | const completeLocale = getCompleteLocale(locale) |
84 | if (isDefaultLocale(completeLocale)) return '' | 84 | if (isDefaultLocale(completeLocale)) return '' |
85 | 85 | ||
diff --git a/client/src/app/core/hotkeys/hotkeys.component.scss b/client/src/app/core/hotkeys/hotkeys.component.scss index 3aa0b6252..a970260c9 100644 --- a/client/src/app/core/hotkeys/hotkeys.component.scss +++ b/client/src/app/core/hotkeys/hotkeys.component.scss | |||
@@ -1,3 +1,6 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
1 | .cfp-hotkeys-container { | 4 | .cfp-hotkeys-container { |
2 | display: flex !important; | 5 | display: flex !important; |
3 | align-items: center; | 6 | align-items: center; |
@@ -23,7 +26,7 @@ | |||
23 | } | 26 | } |
24 | 27 | ||
25 | .cfp-hotkeys-container.fade.in { | 28 | .cfp-hotkeys-container.fade.in { |
26 | z-index: 10002; | 29 | z-index: z(hotkeys); |
27 | visibility: visible; | 30 | visibility: visible; |
28 | opacity: 1; | 31 | opacity: 1; |
29 | } | 32 | } |
@@ -91,7 +94,7 @@ | |||
91 | cursor: pointer; | 94 | cursor: pointer; |
92 | } | 95 | } |
93 | 96 | ||
94 | @media all and (max-width: 500px) { | 97 | @media all and (max-width: $mobile-view) { |
95 | .cfp-hotkeys { | 98 | .cfp-hotkeys { |
96 | font-size: 0.8em; | 99 | font-size: 0.8em; |
97 | } | 100 | } |
diff --git a/client/src/app/core/routing/custom-reuse-strategy.ts b/client/src/app/core/routing/custom-reuse-strategy.ts index a9f61acec..c0f9f04e0 100644 --- a/client/src/app/core/routing/custom-reuse-strategy.ts +++ b/client/src/app/core/routing/custom-reuse-strategy.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router' | 1 | import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router' |
2 | import { Injectable } from '@angular/core' | ||
2 | 3 | ||
4 | @Injectable() | ||
3 | export class CustomReuseStrategy implements RouteReuseStrategy { | 5 | export class CustomReuseStrategy implements RouteReuseStrategy { |
4 | storedRouteHandles = new Map<string, DetachedRouteHandle>() | 6 | storedRouteHandles = new Map<string, DetachedRouteHandle>() |
5 | recentlyUsed: string | 7 | recentlyUsed: string |
@@ -76,6 +78,6 @@ export class CustomReuseStrategy implements RouteReuseStrategy { | |||
76 | } | 78 | } |
77 | 79 | ||
78 | private isReuseEnabled (route: ActivatedRouteSnapshot) { | 80 | private isReuseEnabled (route: ActivatedRouteSnapshot) { |
79 | return route.data.reuse && route.data.reuse.enabled && route.queryParams['a-state'] | 81 | return route.data.reuse && route.data.reuse.enabled && route.queryParams[ 'a-state' ] |
80 | } | 82 | } |
81 | } | 83 | } |
diff --git a/client/src/app/core/routing/preload-selected-modules-list.ts b/client/src/app/core/routing/preload-selected-modules-list.ts index 3bca60317..64af68225 100644 --- a/client/src/app/core/routing/preload-selected-modules-list.ts +++ b/client/src/app/core/routing/preload-selected-modules-list.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { Observable, timer as observableTimer, of as ofObservable } from 'rxjs' | 1 | import { Observable, of as ofObservable, timer as observableTimer } from 'rxjs' |
2 | import { switchMap } from 'rxjs/operators' | 2 | import { switchMap } from 'rxjs/operators' |
3 | import { PreloadingStrategy, Route } from '@angular/router' | 3 | import { PreloadingStrategy, Route } from '@angular/router' |
4 | import { Injectable } from '@angular/core' | ||
4 | 5 | ||
6 | @Injectable() | ||
5 | export class PreloadSelectedModulesList implements PreloadingStrategy { | 7 | export class PreloadSelectedModulesList implements PreloadingStrategy { |
6 | preload (route: Route, load: Function): Observable<any> { | 8 | preload (route: Route, load: Function): Observable<any> { |
7 | if (!route.data || !route.data.preload) return ofObservable(null) | 9 | if (!route.data || !route.data.preload) return ofObservable(null) |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index cdcbcb528..c0e1f08bb 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>() |
@@ -44,6 +47,12 @@ export class ServerService { | |||
44 | css: '' | 47 | css: '' |
45 | } | 48 | } |
46 | }, | 49 | }, |
50 | search: { | ||
51 | remoteUri: { | ||
52 | users: true, | ||
53 | anonymous: false | ||
54 | } | ||
55 | }, | ||
47 | plugin: { | 56 | plugin: { |
48 | registered: [] | 57 | registered: [] |
49 | }, | 58 | }, |
@@ -235,6 +244,10 @@ export class ServerService { | |||
235 | return this.localeObservable.pipe(first()) | 244 | return this.localeObservable.pipe(first()) |
236 | } | 245 | } |
237 | 246 | ||
247 | getServerStats () { | ||
248 | return this.http.get<ServerStats>(ServerService.BASE_STATS_URL) | ||
249 | } | ||
250 | |||
238 | private loadAttributeEnum <T extends string | number> ( | 251 | private loadAttributeEnum <T extends string | number> ( |
239 | baseUrl: string, | 252 | baseUrl: string, |
240 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', | 253 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', |
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index 4fd18f9bd..49e219187 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html | |||
@@ -1,8 +1,4 @@ | |||
1 | <input | 1 | <my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead> |
2 | type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search videos, channels…" | ||
3 | [(ngModel)]="searchValue" (keyup.enter)="doSearch()" | ||
4 | > | ||
5 | <span (click)="doSearch()" class="icon icon-search"></span> | ||
6 | 2 | ||
7 | <a class="upload-button" routerLink="/videos/upload"> | 3 | <a class="upload-button" routerLink="/videos/upload"> |
8 | <my-global-icon iconName="upload"></my-global-icon> | 4 | <my-global-icon iconName="upload"></my-global-icon> |
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index 2bbde74bc..91b390773 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss | |||
@@ -1,51 +1,8 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | #search-video { | 4 | my-search-typeahead { |
5 | @include peertube-input-text($search-input-width); | ||
6 | padding-left: 10px; | ||
7 | margin-right: 15px; | 5 | margin-right: 15px; |
8 | padding-right: 40px; // For the search icon | ||
9 | font-size: 14px; | ||
10 | |||
11 | transition: box-shadow .3s ease; | ||
12 | |||
13 | /* light border style */ | ||
14 | border: 1px solid var(--mainBackgroundColor); | ||
15 | box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px; | ||
16 | |||
17 | &:focus { | ||
18 | box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px; | ||
19 | } | ||
20 | |||
21 | &::placeholder { | ||
22 | color: var(--inputPlaceholderColor); | ||
23 | } | ||
24 | |||
25 | &:focus::placeholder { | ||
26 | opacity: 0 !important; | ||
27 | } | ||
28 | |||
29 | @media screen and (max-width: 800px) { | ||
30 | width: calc(100% - 150px); | ||
31 | } | ||
32 | |||
33 | @media screen and (max-width: 600px) { | ||
34 | width: calc(100% - 70px); | ||
35 | } | ||
36 | } | ||
37 | |||
38 | .icon.icon-search { | ||
39 | @include icon(25px); | ||
40 | height: 21px; | ||
41 | |||
42 | background-color: var(--mainForegroundColor); | ||
43 | mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%; | ||
44 | |||
45 | // yolo | ||
46 | position: absolute; | ||
47 | margin-left: -50px; | ||
48 | margin-top: 5px; | ||
49 | } | 6 | } |
50 | 7 | ||
51 | .upload-button { | 8 | .upload-button { |
@@ -56,10 +13,6 @@ | |||
56 | color: var(--mainBackgroundColor) !important; | 13 | color: var(--mainBackgroundColor) !important; |
57 | margin-right: 25px; | 14 | margin-right: 25px; |
58 | 15 | ||
59 | @media screen and (max-width: 800px) { | ||
60 | margin-right: 0; | ||
61 | } | ||
62 | |||
63 | @media screen and (max-width: 600px) { | 16 | @media screen and (max-width: 600px) { |
64 | margin-right: 10px; | 17 | margin-right: 10px; |
65 | padding: 0 10px; | 18 | padding: 0 10px; |
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts index 92a7eded6..cce76b0d1 100644 --- a/client/src/app/header/header.component.ts +++ b/client/src/app/header/header.component.ts | |||
@@ -1,10 +1,4 @@ | |||
1 | import { filter, first, map, tap } from 'rxjs/operators' | 1 | import { Component } from '@angular/core' |
2 | import { Component, OnInit } from '@angular/core' | ||
3 | import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router' | ||
4 | import { getParameterByName } from '../shared/misc/utils' | ||
5 | import { AuthService, Notifier, ServerService } from '@app/core' | ||
6 | import { of } from 'rxjs' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
8 | 2 | ||
9 | @Component({ | 3 | @Component({ |
10 | selector: 'my-header', | 4 | selector: 'my-header', |
@@ -12,54 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
12 | styleUrls: [ './header.component.scss' ] | 6 | styleUrls: [ './header.component.scss' ] |
13 | }) | 7 | }) |
14 | 8 | ||
15 | export class HeaderComponent implements OnInit { | 9 | export class HeaderComponent {} |
16 | searchValue = '' | ||
17 | ariaLabelTextForSearch = '' | ||
18 | |||
19 | constructor ( | ||
20 | private router: Router, | ||
21 | private route: ActivatedRoute, | ||
22 | private auth: AuthService, | ||
23 | private serverService: ServerService, | ||
24 | private authService: AuthService, | ||
25 | private notifier: Notifier, | ||
26 | private i18n: I18n | ||
27 | ) {} | ||
28 | |||
29 | ngOnInit () { | ||
30 | this.ariaLabelTextForSearch = this.i18n('Search videos, channels') | ||
31 | |||
32 | this.router.events | ||
33 | .pipe( | ||
34 | filter(e => e instanceof NavigationEnd), | ||
35 | map(() => getParameterByName('search', window.location.href)) | ||
36 | ) | ||
37 | .subscribe(searchQuery => this.searchValue = searchQuery || '') | ||
38 | } | ||
39 | |||
40 | doSearch () { | ||
41 | const queryParams: Params = {} | ||
42 | |||
43 | if (window.location.pathname === '/search' && this.route.snapshot.queryParams) { | ||
44 | Object.assign(queryParams, this.route.snapshot.queryParams) | ||
45 | } | ||
46 | |||
47 | Object.assign(queryParams, { search: this.searchValue }) | ||
48 | |||
49 | const o = this.auth.isLoggedIn() | ||
50 | ? this.loadUserLanguagesIfNeeded(queryParams) | ||
51 | : of(true) | ||
52 | |||
53 | o.subscribe(() => this.router.navigate([ '/search' ], { queryParams })) | ||
54 | } | ||
55 | |||
56 | private loadUserLanguagesIfNeeded (queryParams: any) { | ||
57 | if (queryParams && queryParams.languageOneOf) return of(queryParams) | ||
58 | |||
59 | return this.auth.userInformationLoaded | ||
60 | .pipe( | ||
61 | first(), | ||
62 | tap(() => Object.assign(queryParams, { languageOneOf: this.auth.getUser().videoLanguages })) | ||
63 | ) | ||
64 | } | ||
65 | } | ||
diff --git a/client/src/app/header/index.ts b/client/src/app/header/index.ts index d98d2d00a..a882d4d1f 100644 --- a/client/src/app/header/index.ts +++ b/client/src/app/header/index.ts | |||
@@ -1 +1,4 @@ | |||
1 | export * from './header.component' | 1 | export * from './header.component' |
2 | export * from './search-typeahead.component' | ||
3 | export * from './suggestions.component' | ||
4 | export * from './suggestion.component' | ||
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html new file mode 100644 index 000000000..e36809060 --- /dev/null +++ b/client/src/app/header/search-typeahead.component.html | |||
@@ -0,0 +1,53 @@ | |||
1 | <div class="d-inline-flex position-relative" id="typeahead-container"> | ||
2 | <input | ||
3 | type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…" | ||
4 | [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKeyUp($event)" | ||
5 | > | ||
6 | <span class="icon icon-search" (click)="doSearch()"></span> | ||
7 | |||
8 | <div class="position-absolute jump-to-suggestions"> | ||
9 | <!-- suggestions --> | ||
10 | <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions> | ||
11 | |||
12 | <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion --> | ||
13 | <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden"> | ||
14 | <ng-container *ngIf="activeResult.type === 'search-global'"> | ||
15 | <div class="d-flex justify-content-between"> | ||
16 | <label class="small-title" i18n>Global search</label> | ||
17 | <div class="advanced-search-status text-muted"> | ||
18 | <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span> | ||
19 | <i class="glyphicon glyphicon-globe"></i> | ||
20 | </div> | ||
21 | </div> | ||
22 | <div class="text-muted" i18n>Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.</div> | ||
23 | </ng-container> | ||
24 | </div> | ||
25 | |||
26 | <!-- search instructions, when search input is empty --> | ||
27 | <div *ngIf="areInstructionsDisplayed" id="typeahead-instructions" class="overflow-hidden"> | ||
28 | <div class="d-flex justify-content-between"> | ||
29 | <label class="small-title" i18n>Advanced search</label> | ||
30 | <div class="advanced-search-status c-help"> | ||
31 | <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows."> | ||
32 | <span *ngIf="canSearchAnyURI" class="mr-1" i18n>any instance</span> | ||
33 | <span *ngIf="!canSearchAnyURI" class="mr-1" i18n>only followed instances</span> | ||
34 | <i [ngClass]="canSearchAnyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i> | ||
35 | </span> | ||
36 | </div> | ||
37 | </div> | ||
38 | <ul> | ||
39 | <li> | ||
40 | <em>@username@domain</em> <span class="flex-auto text-muted" i18n>account or channel</span> | ||
41 | </li> | ||
42 | <li> | ||
43 | <em>URL</em> <span class="text-muted" i18n>account or channel</span> | ||
44 | </li> | ||
45 | <li> | ||
46 | <em>UUID</em> <span class="text-muted" i18n>video</span> | ||
47 | </li> | ||
48 | </ul> | ||
49 | <span class="text-muted" i18n>Any other text will return matching video, channel or account names.</span> | ||
50 | </div> | ||
51 | </div> | ||
52 | |||
53 | </div> | ||
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss new file mode 100644 index 000000000..a55e78326 --- /dev/null +++ b/client/src/app/header/search-typeahead.component.scss | |||
@@ -0,0 +1,145 @@ | |||
1 | @import '_mixins'; | ||
2 | @import '_variables'; | ||
3 | @import '_bootstrap-variables'; | ||
4 | @import '~bootstrap/scss/mixins/_breakpoints'; | ||
5 | |||
6 | #search-video { | ||
7 | @include peertube-input-text($search-input-width); | ||
8 | padding-left: 10px; | ||
9 | padding-right: 40px; // For the search icon | ||
10 | font-size: 14px; | ||
11 | |||
12 | &::placeholder { | ||
13 | color: var(--inputPlaceholderColor); | ||
14 | } | ||
15 | } | ||
16 | |||
17 | .icon.icon-search { | ||
18 | @include icon(25px); | ||
19 | height: 21px; | ||
20 | |||
21 | background-color: var(--mainForegroundColor); | ||
22 | mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%; | ||
23 | |||
24 | // yolo | ||
25 | position: absolute; | ||
26 | margin-left: -35px; | ||
27 | margin-top: 5px; | ||
28 | } | ||
29 | |||
30 | .jump-to-suggestions { | ||
31 | top: 100%; | ||
32 | left: 0; | ||
33 | z-index: 35; | ||
34 | width: 100%; | ||
35 | } | ||
36 | |||
37 | #typeahead-help, | ||
38 | #typeahead-instructions, | ||
39 | my-suggestions ::ng-deep ul { | ||
40 | border: 1px solid var(--mainBackgroundColor); | ||
41 | border-bottom-right-radius: 3px; | ||
42 | border-bottom-left-radius: 3px; | ||
43 | background: var(--mainBackgroundColor); | ||
44 | transition: .3s ease; | ||
45 | transition-property: box-shadow; | ||
46 | } | ||
47 | |||
48 | #typeahead-help, | ||
49 | #typeahead-instructions { | ||
50 | margin-top: 10px; | ||
51 | width: 100%; | ||
52 | padding: .5rem 1rem; | ||
53 | white-space: normal; | ||
54 | |||
55 | ul { | ||
56 | list-style: none; | ||
57 | padding: 0; | ||
58 | margin-bottom: .5rem; | ||
59 | |||
60 | em { | ||
61 | font-weight: 600; | ||
62 | margin-right: 0.2rem; | ||
63 | font-style: normal; | ||
64 | } | ||
65 | } | ||
66 | } | ||
67 | |||
68 | #typeahead-container { | ||
69 | input { | ||
70 | border: 1px solid var(--mainBackgroundColor) !important; | ||
71 | box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px; | ||
72 | flex-grow: 1; | ||
73 | transition: box-shadow .3s ease, width .2s ease; | ||
74 | } | ||
75 | |||
76 | @media screen and (min-width: $mobile-view) { | ||
77 | margin-left: 10px; | ||
78 | } | ||
79 | |||
80 | @media screen and (max-width: $small-view) { | ||
81 | flex: 1; | ||
82 | |||
83 | input { | ||
84 | width: unset; | ||
85 | } | ||
86 | } | ||
87 | |||
88 | span { | ||
89 | right: 10px; | ||
90 | } | ||
91 | |||
92 | & > div:last-child { | ||
93 | // we have to switch the display and not the opacity, | ||
94 | // to avoid clashing with the rest of the interface. | ||
95 | display: none; | ||
96 | } | ||
97 | |||
98 | &:focus, | ||
99 | ::ng-deep &:focus-within { | ||
100 | & > div:last-child { | ||
101 | @media screen and (min-width: $mobile-view) { | ||
102 | display: initial !important; | ||
103 | } | ||
104 | |||
105 | #typeahead-help, | ||
106 | #typeahead-instructions, | ||
107 | my-suggestions ::ng-deep ul { | ||
108 | box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px; | ||
109 | } | ||
110 | } | ||
111 | |||
112 | ::ng-deep input { | ||
113 | box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px; | ||
114 | border-end-start-radius: 0; | ||
115 | border-end-end-radius: 0; | ||
116 | |||
117 | @include media-breakpoint-up(lg) { | ||
118 | width: 500px; | ||
119 | } | ||
120 | } | ||
121 | } | ||
122 | } | ||
123 | |||
124 | .glyphicon { | ||
125 | top: 3px; | ||
126 | } | ||
127 | |||
128 | .advanced-search-status { | ||
129 | height: max-content; | ||
130 | cursor: default; | ||
131 | |||
132 | &.c-help { | ||
133 | cursor: help; | ||
134 | } | ||
135 | } | ||
136 | |||
137 | .small-title { | ||
138 | @include in-content-small-title; | ||
139 | |||
140 | margin-bottom: .5rem; | ||
141 | } | ||
142 | |||
143 | ::ng-deep my-suggestion { | ||
144 | width: 100%; | ||
145 | } | ||
diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts new file mode 100644 index 000000000..372601fa8 --- /dev/null +++ b/client/src/app/header/search-typeahead.component.ts | |||
@@ -0,0 +1,178 @@ | |||
1 | import { Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core' | ||
2 | import { ActivatedRoute, Params, Router } from '@angular/router' | ||
3 | import { AuthService, ServerService } from '@app/core' | ||
4 | import { first, tap } from 'rxjs/operators' | ||
5 | import { ListKeyManager } from '@angular/cdk/a11y' | ||
6 | import { Result, SuggestionComponent } from './suggestion.component' | ||
7 | import { of } from 'rxjs' | ||
8 | import { ServerConfig } from '@shared/models' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-search-typeahead', | ||
12 | templateUrl: './search-typeahead.component.html', | ||
13 | styleUrls: [ './search-typeahead.component.scss' ] | ||
14 | }) | ||
15 | export class SearchTypeaheadComponent implements OnInit, OnDestroy { | ||
16 | @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement> | ||
17 | |||
18 | hasChannel = false | ||
19 | inChannel = false | ||
20 | newSearch = true | ||
21 | |||
22 | search = '' | ||
23 | serverConfig: ServerConfig | ||
24 | |||
25 | inThisChannelText: string | ||
26 | |||
27 | keyboardEventsManager: ListKeyManager<SuggestionComponent> | ||
28 | results: Result[] = [] | ||
29 | |||
30 | constructor ( | ||
31 | private authService: AuthService, | ||
32 | private router: Router, | ||
33 | private route: ActivatedRoute, | ||
34 | private serverService: ServerService | ||
35 | ) {} | ||
36 | |||
37 | ngOnInit () { | ||
38 | const query = this.route.snapshot.queryParams | ||
39 | if (query['search']) this.search = query['search'] | ||
40 | |||
41 | this.serverService.getConfig() | ||
42 | .subscribe(config => this.serverConfig = config) | ||
43 | } | ||
44 | |||
45 | ngOnDestroy () { | ||
46 | if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() | ||
47 | } | ||
48 | |||
49 | get activeResult () { | ||
50 | return this.keyboardEventsManager?.activeItem?.result | ||
51 | } | ||
52 | |||
53 | get areInstructionsDisplayed () { | ||
54 | return !this.search | ||
55 | } | ||
56 | |||
57 | get showHelp () { | ||
58 | return this.search && this.newSearch && this.activeResult?.type === 'search-global' | ||
59 | } | ||
60 | |||
61 | get canSearchAnyURI () { | ||
62 | if (!this.serverConfig) return false | ||
63 | return this.authService.isLoggedIn() | ||
64 | ? this.serverConfig.search.remoteUri.users | ||
65 | : this.serverConfig.search.remoteUri.anonymous | ||
66 | } | ||
67 | |||
68 | onSearchChange () { | ||
69 | this.computeResults() | ||
70 | } | ||
71 | |||
72 | computeResults () { | ||
73 | this.newSearch = true | ||
74 | let results: Result[] = [] | ||
75 | |||
76 | if (this.search) { | ||
77 | results = [ | ||
78 | /* Channel search is still unimplemented. Uncomment when it is. | ||
79 | { | ||
80 | text: this.search, | ||
81 | type: 'search-channel' | ||
82 | }, | ||
83 | */ | ||
84 | { | ||
85 | text: this.search, | ||
86 | type: 'search-instance', | ||
87 | default: true | ||
88 | }, | ||
89 | /* Global search is still unimplemented. Uncomment when it is. | ||
90 | { | ||
91 | text: this.search, | ||
92 | type: 'search-global' | ||
93 | }, | ||
94 | */ | ||
95 | ...results | ||
96 | ] | ||
97 | } | ||
98 | |||
99 | this.results = results.filter( | ||
100 | (result: Result) => { | ||
101 | // if we're not in a channel or one of its videos/playlits, show all channel-related results | ||
102 | if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') | ||
103 | // if we're in a channel, show all channel-related results except for the channel redirection itself | ||
104 | if (this.inChannel) return result.type !== 'channel' | ||
105 | // all other result types are kept | ||
106 | return true | ||
107 | } | ||
108 | ) | ||
109 | } | ||
110 | |||
111 | setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) { | ||
112 | event.items.forEach(e => { | ||
113 | if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) { | ||
114 | this.keyboardEventsManager.activeItem.active = true | ||
115 | } else { | ||
116 | e.active = false | ||
117 | } | ||
118 | }) | ||
119 | } | ||
120 | |||
121 | initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) { | ||
122 | if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() | ||
123 | |||
124 | this.keyboardEventsManager = new ListKeyManager(event.items) | ||
125 | |||
126 | if (event.index !== undefined) { | ||
127 | this.keyboardEventsManager.setActiveItem(event.index) | ||
128 | } else { | ||
129 | this.keyboardEventsManager.setFirstItemActive() | ||
130 | } | ||
131 | |||
132 | this.keyboardEventsManager.change.subscribe( | ||
133 | _ => this.setEventItems(event) | ||
134 | ) | ||
135 | } | ||
136 | |||
137 | handleKeyUp (event: KeyboardEvent) { | ||
138 | event.stopImmediatePropagation() | ||
139 | if (!this.keyboardEventsManager) return | ||
140 | |||
141 | switch (event.key) { | ||
142 | case 'ArrowDown': | ||
143 | case 'ArrowUp': | ||
144 | this.keyboardEventsManager.onKeydown(event) | ||
145 | break | ||
146 | case 'Enter': | ||
147 | this.newSearch = false | ||
148 | this.doSearch() | ||
149 | break | ||
150 | } | ||
151 | } | ||
152 | |||
153 | doSearch () { | ||
154 | const queryParams: Params = {} | ||
155 | |||
156 | if (window.location.pathname === '/search' && this.route.snapshot.queryParams) { | ||
157 | Object.assign(queryParams, this.route.snapshot.queryParams) | ||
158 | } | ||
159 | |||
160 | Object.assign(queryParams, { search: this.search }) | ||
161 | |||
162 | const o = this.authService.isLoggedIn() | ||
163 | ? this.loadUserLanguagesIfNeeded(queryParams) | ||
164 | : of(true) | ||
165 | |||
166 | o.subscribe(() => this.router.navigate([ '/search' ], { queryParams })) | ||
167 | } | ||
168 | |||
169 | private loadUserLanguagesIfNeeded (queryParams: any) { | ||
170 | if (queryParams && queryParams.languageOneOf) return of(queryParams) | ||
171 | |||
172 | return this.authService.userInformationLoaded | ||
173 | .pipe( | ||
174 | first(), | ||
175 | tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages })) | ||
176 | ) | ||
177 | } | ||
178 | } | ||
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html new file mode 100644 index 000000000..d7ae3450a --- /dev/null +++ b/client/src/app/header/suggestion.component.html | |||
@@ -0,0 +1,22 @@ | |||
1 | <a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active"> | ||
2 | <div class="flex-shrink-0 mr-2 text-center"> | ||
3 | <my-global-icon *ngIf="result.type !== 'channel'" iconName="search"></my-global-icon> | ||
4 | <my-global-icon *ngIf="result.type === 'channel'" iconName="folder"></my-global-icon> | ||
5 | </div> | ||
6 | |||
7 | <img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28"> | ||
8 | |||
9 | <div class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"></div> | ||
10 | |||
11 | <div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6"> | ||
12 | <span *ngIf="result.type === 'search-channel'" i18n>In this channel</span> | ||
13 | <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span> | ||
14 | <span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span> | ||
15 | <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span> | ||
16 | </div> | ||
17 | |||
18 | <div *ngIf="result.type === 'channel'" aria-hidden="true" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6 d-on-nav-focus" i18n> | ||
19 | Jump to channel | ||
20 | <span class="d-inline-block ml-1 v-align-middle">↵</span> | ||
21 | </div> | ||
22 | </a> \ No newline at end of file | ||
diff --git a/client/src/app/header/suggestion.component.scss b/client/src/app/header/suggestion.component.scss new file mode 100644 index 000000000..1de2f43bd --- /dev/null +++ b/client/src/app/header/suggestion.component.scss | |||
@@ -0,0 +1,32 @@ | |||
1 | @import '_mixins'; | ||
2 | |||
3 | a { | ||
4 | @include disable-default-a-behaviour; | ||
5 | width: 100%; | ||
6 | |||
7 | &, &:hover { | ||
8 | color: var(--mainForegroundColor); | ||
9 | |||
10 | &.focus-visible { | ||
11 | background-color: var(--mainHoverColor); | ||
12 | color: var(--mainBackgroundColor); | ||
13 | } | ||
14 | } | ||
15 | } | ||
16 | |||
17 | .bg-gray { | ||
18 | background-color: var(--mainBackgroundColor); | ||
19 | } | ||
20 | |||
21 | .text-gray-light { | ||
22 | color: var(--mainForegroundColor); | ||
23 | } | ||
24 | |||
25 | my-global-icon { | ||
26 | width: 17px; | ||
27 | position: relative; | ||
28 | top: -2px; | ||
29 | margin: 5px; | ||
30 | |||
31 | @include apply-svg-color(var(--mainForegroundColor)); | ||
32 | } | ||
diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts new file mode 100644 index 000000000..69641b511 --- /dev/null +++ b/client/src/app/header/suggestion.component.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core' | ||
2 | import { RouterLink } from '@angular/router' | ||
3 | import { ListKeyManagerOption } from '@angular/cdk/a11y' | ||
4 | |||
5 | export type Result = { | ||
6 | text: string | ||
7 | type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any' | ||
8 | routerLink?: RouterLink, | ||
9 | default?: boolean | ||
10 | } | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-suggestion', | ||
14 | templateUrl: './suggestion.component.html', | ||
15 | styleUrls: [ './suggestion.component.scss' ], | ||
16 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | ||
18 | export class SuggestionComponent implements OnInit, ListKeyManagerOption { | ||
19 | @Input() result: Result | ||
20 | @Input() highlight: string | ||
21 | @Output() selected = new EventEmitter() | ||
22 | |||
23 | disabled = false | ||
24 | active = false | ||
25 | |||
26 | getLabel () { | ||
27 | return this.result.text | ||
28 | } | ||
29 | |||
30 | ngOnInit () { | ||
31 | if (this.result.default) this.active = true | ||
32 | } | ||
33 | |||
34 | selectItem () { | ||
35 | this.selected.emit(this.result) | ||
36 | } | ||
37 | } | ||
diff --git a/client/src/app/header/suggestions.component.html b/client/src/app/header/suggestions.component.html new file mode 100644 index 000000000..8d017d78d --- /dev/null +++ b/client/src/app/header/suggestions.component.html | |||
@@ -0,0 +1,6 @@ | |||
1 | <ul role="listbox" class="p-0 m-0"> | ||
2 | <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5" | ||
3 | role="option" aria-selected="true" (mouseenter)="hoverItem(i)"> | ||
4 | <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion> | ||
5 | </li> | ||
6 | </ul> \ No newline at end of file | ||
diff --git a/client/src/app/header/suggestions.component.ts b/client/src/app/header/suggestions.component.ts new file mode 100644 index 000000000..ee3ef73c2 --- /dev/null +++ b/client/src/app/header/suggestions.component.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core' | ||
2 | import { SuggestionComponent } from './suggestion.component' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-suggestions', | ||
6 | templateUrl: './suggestions.component.html', | ||
7 | changeDetection: ChangeDetectionStrategy.OnPush | ||
8 | }) | ||
9 | export class SuggestionsComponent implements AfterViewInit { | ||
10 | @Input() results: any[] | ||
11 | @Input() highlight: string | ||
12 | @ViewChildren(SuggestionComponent) listItems: QueryList<SuggestionComponent> | ||
13 | @Output() init = new EventEmitter() | ||
14 | |||
15 | ngAfterViewInit () { | ||
16 | this.listItems.changes.subscribe( | ||
17 | _ => this.init.emit({ items: this.listItems }) | ||
18 | ) | ||
19 | } | ||
20 | |||
21 | hoverItem (index: number) { | ||
22 | this.init.emit({ items: this.listItems, index: index }) | ||
23 | } | ||
24 | } | ||
diff --git a/client/src/app/menu/avatar-notification.component.html b/client/src/app/menu/avatar-notification.component.html index 7975afba5..df2a102a3 100644 --- a/client/src/app/menu/avatar-notification.component.html +++ b/client/src/app/menu/avatar-notification.component.html | |||
@@ -30,7 +30,7 @@ | |||
30 | </div> | 30 | </div> |
31 | 31 | ||
32 | <my-user-notifications | 32 | <my-user-notifications |
33 | [ignoreLoadingBar]="true" [infiniteScroll]="false" itemsPerPage="10" | 33 | [ignoreLoadingBar]="true" [infiniteScroll]="false" [itemsPerPage]="10" |
34 | [markAllAsReadSubject]="markAllAsReadSubject" (notificationsLoaded)="onNotificationLoaded()" | 34 | [markAllAsReadSubject]="markAllAsReadSubject" (notificationsLoaded)="onNotificationLoaded()" |
35 | ></my-user-notifications> | 35 | ></my-user-notifications> |
36 | 36 | ||
diff --git a/client/src/app/menu/avatar-notification.component.ts b/client/src/app/menu/avatar-notification.component.ts index 989a11849..c447f031c 100644 --- a/client/src/app/menu/avatar-notification.component.ts +++ b/client/src/app/menu/avatar-notification.component.ts | |||
@@ -6,7 +6,6 @@ import { Notifier, UserNotificationSocket } from '@app/core' | |||
6 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' |
7 | import { NavigationEnd, Router } from '@angular/router' | 7 | import { NavigationEnd, Router } from '@angular/router' |
8 | import { filter } from 'rxjs/operators' | 8 | import { filter } from 'rxjs/operators' |
9 | import { UserNotificationsComponent } from '@app/shared' | ||
10 | 9 | ||
11 | @Component({ | 10 | @Component({ |
12 | selector: 'my-avatar-notification', | 11 | selector: 'my-avatar-notification', |
diff --git a/client/src/app/menu/language-chooser.component.ts b/client/src/app/menu/language-chooser.component.ts index 4a6e4c75a..43f622dfb 100644 --- a/client/src/app/menu/language-chooser.component.ts +++ b/client/src/app/menu/language-chooser.component.ts | |||
@@ -21,7 +21,7 @@ export class LanguageChooserComponent { | |||
21 | } | 21 | } |
22 | 22 | ||
23 | show () { | 23 | show () { |
24 | this.modalService.open(this.modal) | 24 | this.modalService.open(this.modal, { centered: true }) |
25 | } | 25 | } |
26 | 26 | ||
27 | buildLanguageLink (lang: { id: string }) { | 27 | buildLanguageLink (lang: { id: string }) { |
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 675fb597d..91422df77 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -8,10 +8,10 @@ | |||
8 | <a *ngIf="user.account" [routerLink]="[ '/accounts', user.account.nameWithHost ]" class="logged-in-display-name">{{ user.account?.displayName }}</a> | 8 | <a *ngIf="user.account" [routerLink]="[ '/accounts', user.account.nameWithHost ]" class="logged-in-display-name">{{ user.account?.displayName }}</a> |
9 | <a *ngIf="!user.account" routerLink="/my-account/settings" class="logged-in-display-name">{{ user.account?.displayName }}</a> | 9 | <a *ngIf="!user.account" routerLink="/my-account/settings" class="logged-in-display-name">{{ user.account?.displayName }}</a> |
10 | 10 | ||
11 | <div ngxClipboard [cbContent]="user.account?.nameWithHost" class="logged-in-username">{{ user.username }}</div> | 11 | <div class="logged-in-username">{{ user.username }}</div> |
12 | </div> | 12 | </div> |
13 | 13 | ||
14 | <div class="logged-in-more" ngbDropdown placement="bottom-right auto"> | 14 | <div class="logged-in-more" ngbDropdown placement="bottom-right auto" container="body"> |
15 | <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button"></my-global-icon> | 15 | <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button"></my-global-icon> |
16 | 16 | ||
17 | <div ngbDropdownMenu> | 17 | <div ngbDropdownMenu> |
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index 2963d4d19..cb5f90723 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss | |||
@@ -6,7 +6,8 @@ | |||
6 | height: calc(100vh - #{$header-height}); | 6 | height: calc(100vh - #{$header-height}); |
7 | padding: 0; | 7 | padding: 0; |
8 | width: $menu-width; | 8 | width: $menu-width; |
9 | z-index: 10000; | 9 | z-index: z(menu); |
10 | scrollbar-color: var(--actionButtonColor) var(--menuBackgroundColor); | ||
10 | } | 11 | } |
11 | 12 | ||
12 | menu { | 13 | menu { |
diff --git a/client/src/app/modal/instance-config-warning-modal.component.ts b/client/src/app/modal/instance-config-warning-modal.component.ts index 742a7dd41..5e1433548 100644 --- a/client/src/app/modal/instance-config-warning-modal.component.ts +++ b/client/src/app/modal/instance-config-warning-modal.component.ts | |||
@@ -24,7 +24,7 @@ export class InstanceConfigWarningModalComponent { | |||
24 | show (about: About) { | 24 | show (about: About) { |
25 | this.about = about | 25 | this.about = about |
26 | 26 | ||
27 | const ref = this.modalService.open(this.modal) | 27 | const ref = this.modalService.open(this.modal, { centered: true }) |
28 | 28 | ||
29 | ref.result.finally(() => { | 29 | ref.result.finally(() => { |
30 | if (this.stopDisplayModal === true) this.doNotOpenAgain() | 30 | if (this.stopDisplayModal === true) this.doNotOpenAgain() |
diff --git a/client/src/app/modal/welcome-modal.component.ts b/client/src/app/modal/welcome-modal.component.ts index 19a147b85..e022776e3 100644 --- a/client/src/app/modal/welcome-modal.component.ts +++ b/client/src/app/modal/welcome-modal.component.ts | |||
@@ -18,7 +18,8 @@ export class WelcomeModalComponent { | |||
18 | ) { } | 18 | ) { } |
19 | 19 | ||
20 | show () { | 20 | show () { |
21 | this.modalService.open(this.modal,{ | 21 | this.modalService.open(this.modal, { |
22 | centered: true, | ||
22 | backdrop: 'static', | 23 | backdrop: 'static', |
23 | keyboard: false, | 24 | keyboard: false, |
24 | size: 'lg' | 25 | size: 'lg' |
diff --git a/client/src/app/search/search-filters.component.html b/client/src/app/search/search-filters.component.html index 07fb2c048..60680c7bd 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> |
@@ -146,7 +146,7 @@ | |||
146 | [(ngModel)]="advancedSearch.tagsAllOf" name="tagsAllOf" id="tagsAllOf" | 146 | [(ngModel)]="advancedSearch.tagsAllOf" name="tagsAllOf" id="tagsAllOf" |
147 | [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | 147 | [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" |
148 | i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag" | 148 | i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag" |
149 | maxItems="5" modelAsStrings="true" | 149 | [maxItems]="5" [modelAsStrings]="true" |
150 | ></tag-input> | 150 | ></tag-input> |
151 | </div> | 151 | </div> |
152 | 152 | ||
@@ -159,7 +159,7 @@ | |||
159 | [(ngModel)]="advancedSearch.tagsOneOf" name="tagsOneOf" id="tagsOneOf" | 159 | [(ngModel)]="advancedSearch.tagsOneOf" name="tagsOneOf" id="tagsOneOf" |
160 | [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | 160 | [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" |
161 | i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag" | 161 | i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag" |
162 | maxItems="5" modelAsStrings="true" | 162 | [maxItems]="5" [modelAsStrings]="true" |
163 | ></tag-input> | 163 | ></tag-input> |
164 | </div> | 164 | </div> |
165 | </div> | 165 | </div> |
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts index dfd8d8823..075994dd3 100644 --- a/client/src/app/search/search.component.ts +++ b/client/src/app/search/search.component.ts | |||
@@ -141,7 +141,8 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
141 | return this.advancedSearch.size() | 141 | return this.advancedSearch.size() |
142 | } | 142 | } |
143 | 143 | ||
144 | removeVideoFromArray (video: Video) { | 144 | // Add VideoChannel for typings, but the template already checks "video" argument is a video |
145 | removeVideoFromArray (video: Video | VideoChannel) { | ||
145 | this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) | 146 | this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) |
146 | } | 147 | } |
147 | 148 | ||
diff --git a/client/src/app/shared/angular/highlight.pipe.ts b/client/src/app/shared/angular/highlight.pipe.ts new file mode 100644 index 000000000..fb6042280 --- /dev/null +++ b/client/src/app/shared/angular/highlight.pipe.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import { PipeTransform, Pipe } from '@angular/core' | ||
2 | import { SafeHtml } from '@angular/platform-browser' | ||
3 | |||
4 | // Thanks https://gist.github.com/adamrecsko/0f28f474eca63e0279455476cc11eca7#gistcomment-2917369 | ||
5 | @Pipe({ name: 'highlight' }) | ||
6 | export class HighlightPipe implements PipeTransform { | ||
7 | /* use this for single match search */ | ||
8 | static SINGLE_MATCH = 'Single-Match' | ||
9 | /* use this for single match search with a restriction that target should start with search string */ | ||
10 | static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match' | ||
11 | /* use this for global search */ | ||
12 | static MULTI_MATCH = 'Multi-Match' | ||
13 | |||
14 | // tslint:disable-next-line:no-empty | ||
15 | constructor () {} | ||
16 | |||
17 | transform ( | ||
18 | contentString: string = null, | ||
19 | stringToHighlight: string = null, | ||
20 | option = 'Single-And-StartsWith-Match', | ||
21 | caseSensitive = false, | ||
22 | highlightStyleName = 'search-highlight' | ||
23 | ): SafeHtml { | ||
24 | if (stringToHighlight && contentString && option) { | ||
25 | let regex: any = '' | ||
26 | const caseFlag: string = !caseSensitive ? 'i' : '' | ||
27 | switch (option) { | ||
28 | case 'Single-Match': { | ||
29 | regex = new RegExp(stringToHighlight, caseFlag) | ||
30 | break | ||
31 | } | ||
32 | case 'Single-And-StartsWith-Match': { | ||
33 | regex = new RegExp('^' + stringToHighlight, caseFlag) | ||
34 | break | ||
35 | } | ||
36 | case 'Multi-Match': { | ||
37 | regex = new RegExp(stringToHighlight, 'g' + caseFlag) | ||
38 | break | ||
39 | } | ||
40 | default: { | ||
41 | // default will be a global case-insensitive match | ||
42 | regex = new RegExp(stringToHighlight, 'gi') | ||
43 | } | ||
44 | } | ||
45 | const replaced = contentString.replace( | ||
46 | regex, | ||
47 | (match) => `<span class="${highlightStyleName}">${match}</span>` | ||
48 | ) | ||
49 | return replaced | ||
50 | } else { | ||
51 | return contentString | ||
52 | } | ||
53 | } | ||
54 | } | ||
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts index a8b3ab16c..6649b092a 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.ts +++ b/client/src/app/shared/buttons/action-dropdown.component.ts | |||
@@ -34,10 +34,10 @@ export class ActionDropdownComponent<T> { | |||
34 | @Input() label: string | 34 | @Input() label: string |
35 | @Input() theme: DropdownTheme = 'grey' | 35 | @Input() theme: DropdownTheme = 'grey' |
36 | 36 | ||
37 | getActions () { | 37 | getActions (): DropdownAction<T>[][] { |
38 | if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions | 38 | if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions as DropdownAction<T>[][] |
39 | 39 | ||
40 | return [ this.actions ] | 40 | return [ this.actions as DropdownAction<T>[] ] |
41 | } | 41 | } |
42 | 42 | ||
43 | areActionsDisplayed (actions: Array<DropdownAction<T> | DropdownAction<T>[]>, entry: T): boolean { | 43 | areActionsDisplayed (actions: Array<DropdownAction<T> | DropdownAction<T>[]>, entry: T): boolean { |
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/buttons/edit-button.component.ts b/client/src/app/shared/buttons/edit-button.component.ts index 1fe4f7b30..9cfe1a3bb 100644 --- a/client/src/app/shared/buttons/edit-button.component.ts +++ b/client/src/app/shared/buttons/edit-button.component.ts | |||
@@ -8,5 +8,5 @@ import { Component, Input } from '@angular/core' | |||
8 | 8 | ||
9 | export class EditButtonComponent { | 9 | export class EditButtonComponent { |
10 | @Input() label: string | 10 | @Input() label: string |
11 | @Input() routerLink: string[] = [] | 11 | @Input() routerLink: string[] | string = [] |
12 | } | 12 | } |
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/forms/form-validators/user-validators.service.ts b/client/src/app/shared/forms/form-validators/user-validators.service.ts index 4dff3e422..13b9228d4 100644 --- a/client/src/app/shared/forms/form-validators/user-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/user-validators.service.ts | |||
@@ -8,6 +8,7 @@ export class UserValidatorsService { | |||
8 | readonly USER_USERNAME: BuildFormValidator | 8 | readonly USER_USERNAME: BuildFormValidator |
9 | readonly USER_EMAIL: BuildFormValidator | 9 | readonly USER_EMAIL: BuildFormValidator |
10 | readonly USER_PASSWORD: BuildFormValidator | 10 | readonly USER_PASSWORD: BuildFormValidator |
11 | readonly USER_PASSWORD_OPTIONAL: BuildFormValidator | ||
11 | readonly USER_CONFIRM_PASSWORD: BuildFormValidator | 12 | readonly USER_CONFIRM_PASSWORD: BuildFormValidator |
12 | readonly USER_VIDEO_QUOTA: BuildFormValidator | 13 | readonly USER_VIDEO_QUOTA: BuildFormValidator |
13 | readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator | 14 | readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator |
@@ -56,6 +57,17 @@ export class UserValidatorsService { | |||
56 | } | 57 | } |
57 | } | 58 | } |
58 | 59 | ||
60 | this.USER_PASSWORD_OPTIONAL = { | ||
61 | VALIDATORS: [ | ||
62 | Validators.minLength(6), | ||
63 | Validators.maxLength(255) | ||
64 | ], | ||
65 | MESSAGES: { | ||
66 | 'minlength': this.i18n('Password must be at least 6 characters long.'), | ||
67 | 'maxlength': this.i18n('Password cannot be more than 255 characters long.') | ||
68 | } | ||
69 | } | ||
70 | |||
59 | this.USER_CONFIRM_PASSWORD = { | 71 | this.USER_CONFIRM_PASSWORD = { |
60 | VALIDATORS: [], | 72 | VALIDATORS: [], |
61 | MESSAGES: { | 73 | MESSAGES: { |
diff --git a/client/src/app/shared/forms/input-readonly-copy.component.html b/client/src/app/shared/forms/input-readonly-copy.component.html index 27571b63f..b6a56ec44 100644 --- a/client/src/app/shared/forms/input-readonly-copy.component.html +++ b/client/src/app/shared/forms/input-readonly-copy.component.html | |||
@@ -2,7 +2,7 @@ | |||
2 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control readonly" readonly [value]="value" /> | 2 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control readonly" readonly [value]="value" /> |
3 | 3 | ||
4 | <div class="input-group-append"> | 4 | <div class="input-group-append"> |
5 | <button [ngxClipboard]="urlInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | 5 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> |
6 | <span class="glyphicon glyphicon-copy"></span> | 6 | <span class="glyphicon glyphicon-copy"></span> |
7 | </button> | 7 | </button> |
8 | </div> | 8 | </div> |
diff --git a/client/src/app/shared/forms/markdown-textarea.component.ts b/client/src/app/shared/forms/markdown-textarea.component.ts index 19cd37573..cbcfdfe78 100644 --- a/client/src/app/shared/forms/markdown-textarea.component.ts +++ b/client/src/app/shared/forms/markdown-textarea.component.ts | |||
@@ -21,7 +21,7 @@ import { MarkdownService } from '@app/shared/renderer' | |||
21 | 21 | ||
22 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | 22 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { |
23 | @Input() content = '' | 23 | @Input() content = '' |
24 | @Input() classes: string[] = [] | 24 | @Input() classes: string[] | { [klass: string]: any[] | any } = [] |
25 | @Input() textareaWidth = '100%' | 25 | @Input() textareaWidth = '100%' |
26 | @Input() textareaHeight = '150px' | 26 | @Input() textareaHeight = '150px' |
27 | @Input() previewColumn = false | 27 | @Input() previewColumn = false |
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/images/preview-upload.component.ts b/client/src/app/shared/images/preview-upload.component.ts index f56f5b1f8..85a2173e9 100644 --- a/client/src/app/shared/images/preview-upload.component.ts +++ b/client/src/app/shared/images/preview-upload.component.ts | |||
@@ -26,7 +26,7 @@ export class PreviewUploadComponent implements OnInit, ControlValueAccessor { | |||
26 | allowedExtensionsMessage = '' | 26 | allowedExtensionsMessage = '' |
27 | 27 | ||
28 | private serverConfig: ServerConfig | 28 | private serverConfig: ServerConfig |
29 | private file: File | 29 | private file: Blob |
30 | 30 | ||
31 | constructor ( | 31 | constructor ( |
32 | private sanitizer: DomSanitizer, | 32 | private sanitizer: DomSanitizer, |
@@ -49,7 +49,7 @@ export class PreviewUploadComponent implements OnInit, ControlValueAccessor { | |||
49 | this.allowedExtensionsMessage = this.videoImageExtensions.join(', ') | 49 | this.allowedExtensionsMessage = this.videoImageExtensions.join(', ') |
50 | } | 50 | } |
51 | 51 | ||
52 | onFileChanged (file: File) { | 52 | onFileChanged (file: Blob) { |
53 | this.file = file | 53 | this.file = file |
54 | 54 | ||
55 | this.propagateChange(this.file) | 55 | this.propagateChange(this.file) |
diff --git a/client/src/app/shared/instance/instance-features-table.component.html b/client/src/app/shared/instance/instance-features-table.component.html index fd8b3354f..51a56d414 100644 --- a/client/src/app/shared/instance/instance-features-table.component.html +++ b/client/src/app/shared/instance/instance-features-table.component.html | |||
@@ -91,5 +91,16 @@ | |||
91 | <my-feature-boolean [value]="serverConfig.tracker.enabled"></my-feature-boolean> | 91 | <my-feature-boolean [value]="serverConfig.tracker.enabled"></my-feature-boolean> |
92 | </td> | 92 | </td> |
93 | </tr> | 93 | </tr> |
94 | |||
95 | <tr> | ||
96 | <td i18n class="label" colspan="2">Search</td> | ||
97 | </tr> | ||
98 | |||
99 | <tr> | ||
100 | <td i18n class="sub-label">Users can resolve distant content</td> | ||
101 | <td> | ||
102 | <my-feature-boolean [value]="serverConfig.search.remoteUri.users"></my-feature-boolean> | ||
103 | </td> | ||
104 | </tr> | ||
94 | </table> | 105 | </table> |
95 | </div> | 106 | </div> |
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/misc/list-overflow.component.html b/client/src/app/shared/misc/list-overflow.component.html new file mode 100644 index 000000000..986572801 --- /dev/null +++ b/client/src/app/shared/misc/list-overflow.component.html | |||
@@ -0,0 +1,35 @@ | |||
1 | <div #itemsParent class="d-flex align-items-center text-nowrap w-100 list-overflow-parent"> | ||
2 | <span [id]="getId(id)" #itemsRendered *ngFor="let item of items; index as id"> | ||
3 | <ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container> | ||
4 | </span> | ||
5 | |||
6 | <ng-container *ngIf="isMenuDisplayed()"> | ||
7 | <button *ngIf="isInMobileView" class="btn btn-outline-secondary btn-sm list-overflow-menu" (click)="toggleModal()"> | ||
8 | <span class="glyphicon glyphicon-chevron-down"></span> | ||
9 | </button> | ||
10 | |||
11 | <div *ngIf="!isInMobileView" class="list-overflow-menu" ngbDropdown container="body" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)" (mouseenter)="openDropdownOnHover(dropdown)"> | ||
12 | <button class="btn btn-outline-secondary btn-sm" [ngClass]="{ routeActive: active }" | ||
13 | ngbDropdownAnchor (click)="dropdownAnchorClicked(dropdown)" role="button" | ||
14 | > | ||
15 | <span class="glyphicon glyphicon-chevron-down"></span> | ||
16 | </button> | ||
17 | |||
18 | <div ngbDropdownMenu> | ||
19 | <a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length" | ||
20 | [routerLink]="item.routerLink" routerLinkActive="active" class="dropdown-item"> | ||
21 | {{ item.label }} | ||
22 | </a> | ||
23 | </div> | ||
24 | </div> | ||
25 | </ng-container> | ||
26 | </div > | ||
27 | |||
28 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | ||
29 | <div class="modal-body"> | ||
30 | <a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length" | ||
31 | [routerLink]="item.routerLink" routerLinkActive="active" (click)="dismissOtherModals()"> | ||
32 | {{ item.label }} | ||
33 | </a> | ||
34 | </div> | ||
35 | </ng-template> | ||
diff --git a/client/src/app/shared/misc/list-overflow.component.scss b/client/src/app/shared/misc/list-overflow.component.scss new file mode 100644 index 000000000..e26100aca --- /dev/null +++ b/client/src/app/shared/misc/list-overflow.component.scss | |||
@@ -0,0 +1,61 @@ | |||
1 | @import '_mixins'; | ||
2 | |||
3 | :host { | ||
4 | width: 100%; | ||
5 | } | ||
6 | |||
7 | .list-overflow-parent { | ||
8 | overflow: hidden; | ||
9 | } | ||
10 | |||
11 | .list-overflow-menu { | ||
12 | position: absolute; | ||
13 | right: 0; | ||
14 | } | ||
15 | |||
16 | button { | ||
17 | width: 30px; | ||
18 | border: none; | ||
19 | |||
20 | &::after { | ||
21 | display: none; | ||
22 | } | ||
23 | |||
24 | &.routeActive { | ||
25 | &::after { | ||
26 | display: inherit; | ||
27 | border: 2px solid var(--mainColor); | ||
28 | position: relative; | ||
29 | right: 95%; | ||
30 | top: 50%; | ||
31 | } | ||
32 | } | ||
33 | } | ||
34 | |||
35 | ::ng-deep .dropdown-menu { | ||
36 | margin-top: 0 !important; | ||
37 | position: static; | ||
38 | right: auto; | ||
39 | bottom: auto | ||
40 | } | ||
41 | |||
42 | .modal-body { | ||
43 | a { | ||
44 | @include disable-default-a-behaviour; | ||
45 | |||
46 | color: currentColor; | ||
47 | box-sizing: border-box; | ||
48 | display: block; | ||
49 | font-size: 1.2rem; | ||
50 | padding: 9px 12px; | ||
51 | text-align: initial; | ||
52 | text-transform: unset; | ||
53 | width: 100%; | ||
54 | |||
55 | &.active { | ||
56 | color: var(--mainBackgroundColor) !important; | ||
57 | background-color: var(--mainHoverColor); | ||
58 | opacity: .9; | ||
59 | } | ||
60 | } | ||
61 | } | ||
diff --git a/client/src/app/shared/misc/list-overflow.component.ts b/client/src/app/shared/misc/list-overflow.component.ts new file mode 100644 index 000000000..c493ab795 --- /dev/null +++ b/client/src/app/shared/misc/list-overflow.component.ts | |||
@@ -0,0 +1,119 @@ | |||
1 | import { | ||
2 | AfterViewInit, | ||
3 | ChangeDetectionStrategy, | ||
4 | ChangeDetectorRef, | ||
5 | Component, | ||
6 | ElementRef, | ||
7 | HostListener, | ||
8 | Input, | ||
9 | QueryList, | ||
10 | TemplateRef, | ||
11 | ViewChild, | ||
12 | ViewChildren | ||
13 | } from '@angular/core' | ||
14 | import { NgbDropdown, NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
15 | import { lowerFirst, uniqueId } from 'lodash-es' | ||
16 | import { ScreenService } from './screen.service' | ||
17 | import { take } from 'rxjs/operators' | ||
18 | |||
19 | export interface ListOverflowItem { | ||
20 | label: string | ||
21 | routerLink: string | any[] | ||
22 | } | ||
23 | |||
24 | @Component({ | ||
25 | selector: 'list-overflow', | ||
26 | templateUrl: './list-overflow.component.html', | ||
27 | styleUrls: [ './list-overflow.component.scss' ], | ||
28 | changeDetection: ChangeDetectionStrategy.OnPush | ||
29 | }) | ||
30 | export class ListOverflowComponent<T extends ListOverflowItem> implements AfterViewInit { | ||
31 | @ViewChild('modal', { static: true }) modal: ElementRef | ||
32 | @ViewChild('itemsParent', { static: true }) parent: ElementRef<HTMLDivElement> | ||
33 | @ViewChildren('itemsRendered') itemsRendered: QueryList<ElementRef> | ||
34 | @Input() items: T[] | ||
35 | @Input() itemTemplate: TemplateRef<{item: T}> | ||
36 | |||
37 | showItemsUntilIndexExcluded: number | ||
38 | active = false | ||
39 | isInTouchScreen = false | ||
40 | isInMobileView = false | ||
41 | |||
42 | private openedOnHover = false | ||
43 | |||
44 | constructor ( | ||
45 | private cdr: ChangeDetectorRef, | ||
46 | private modalService: NgbModal, | ||
47 | private screenService: ScreenService | ||
48 | ) {} | ||
49 | |||
50 | ngAfterViewInit () { | ||
51 | setTimeout(() => this.onWindowResize(), 0) | ||
52 | } | ||
53 | |||
54 | isMenuDisplayed () { | ||
55 | return !!this.showItemsUntilIndexExcluded | ||
56 | } | ||
57 | |||
58 | @HostListener('window:resize', ['$event']) | ||
59 | onWindowResize () { | ||
60 | this.isInTouchScreen = !!this.screenService.isInTouchScreen() | ||
61 | this.isInMobileView = !!this.screenService.isInMobileView() | ||
62 | |||
63 | const parentWidth = this.parent.nativeElement.getBoundingClientRect().width | ||
64 | let showItemsUntilIndexExcluded: number | ||
65 | let accWidth = 0 | ||
66 | |||
67 | for (const [index, el] of this.itemsRendered.toArray().entries()) { | ||
68 | accWidth += el.nativeElement.getBoundingClientRect().width | ||
69 | if (showItemsUntilIndexExcluded === undefined) { | ||
70 | showItemsUntilIndexExcluded = (parentWidth < accWidth) ? index : undefined | ||
71 | } | ||
72 | |||
73 | const e = document.getElementById(this.getId(index)) | ||
74 | const shouldBeVisible = showItemsUntilIndexExcluded ? index < showItemsUntilIndexExcluded : true | ||
75 | e.style.visibility = shouldBeVisible ? 'inherit' : 'hidden' | ||
76 | } | ||
77 | |||
78 | this.showItemsUntilIndexExcluded = showItemsUntilIndexExcluded | ||
79 | this.cdr.markForCheck() | ||
80 | } | ||
81 | |||
82 | openDropdownOnHover (dropdown: NgbDropdown) { | ||
83 | this.openedOnHover = true | ||
84 | dropdown.open() | ||
85 | |||
86 | // Menu was closed | ||
87 | dropdown.openChange | ||
88 | .pipe(take(1)) | ||
89 | .subscribe(() => this.openedOnHover = false) | ||
90 | } | ||
91 | |||
92 | dropdownAnchorClicked (dropdown: NgbDropdown) { | ||
93 | if (this.openedOnHover) { | ||
94 | this.openedOnHover = false | ||
95 | return | ||
96 | } | ||
97 | |||
98 | return dropdown.toggle() | ||
99 | } | ||
100 | |||
101 | closeDropdownIfHovered (dropdown: NgbDropdown) { | ||
102 | if (this.openedOnHover === false) return | ||
103 | |||
104 | dropdown.close() | ||
105 | this.openedOnHover = false | ||
106 | } | ||
107 | |||
108 | toggleModal () { | ||
109 | this.modalService.open(this.modal, { centered: true }) | ||
110 | } | ||
111 | |||
112 | dismissOtherModals () { | ||
113 | this.modalService.dismissAll() | ||
114 | } | ||
115 | |||
116 | getId (id: number | string = uniqueId()): string { | ||
117 | return lowerFirst(this.constructor.name) + '_' + id | ||
118 | } | ||
119 | } | ||
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.ts b/client/src/app/shared/moderation/user-ban-modal.component.ts index cf0e1577a..1647e3691 100644 --- a/client/src/app/shared/moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/moderation/user-ban-modal.component.ts | |||
@@ -39,7 +39,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
39 | 39 | ||
40 | openModal (user: User | User[]) { | 40 | openModal (user: User | User[]) { |
41 | this.usersToBan = user | 41 | this.usersToBan = user |
42 | this.openedModal = this.modalService.open(this.modal) | 42 | this.openedModal = this.modalService.open(this.modal, { centered: true }) |
43 | } | 43 | } |
44 | 44 | ||
45 | hide () { | 45 | hide () { |
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts index 11d8588f4..9197556b0 100644 --- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts | |||
@@ -14,7 +14,7 @@ import { ServerConfig } from '@shared/models' | |||
14 | templateUrl: './user-moderation-dropdown.component.html' | 14 | templateUrl: './user-moderation-dropdown.component.html' |
15 | }) | 15 | }) |
16 | export class UserModerationDropdownComponent implements OnInit, OnChanges { | 16 | export class UserModerationDropdownComponent implements OnInit, OnChanges { |
17 | @ViewChild('userBanModal', { static: false }) userBanModal: UserBanModalComponent | 17 | @ViewChild('userBanModal') userBanModal: UserBanModalComponent |
18 | 18 | ||
19 | @Input() user: User | 19 | @Input() user: User |
20 | @Input() account: Account | 20 | @Input() account: Account |
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/rest/rest-table.ts b/client/src/app/shared/rest/rest-table.ts index c180346af..a33e99e25 100644 --- a/client/src/app/shared/rest/rest-table.ts +++ b/client/src/app/shared/rest/rest-table.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' | 1 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' |
2 | import { LazyLoadEvent } from 'primeng/components/common/lazyloadevent' | 2 | import { LazyLoadEvent, SortMeta } from 'primeng/api' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
4 | import { RestPagination } from './rest-pagination' | 3 | import { RestPagination } from './rest-pagination' |
5 | import { Subject } from 'rxjs' | 4 | import { Subject } from 'rxjs' |
6 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 5 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
@@ -66,8 +65,9 @@ export abstract class RestTable { | |||
66 | }) | 65 | }) |
67 | } | 66 | } |
68 | 67 | ||
69 | onSearch (search: string) { | 68 | onSearch (event: Event) { |
70 | this.searchStream.next(search) | 69 | const target = event.target as HTMLInputElement |
70 | this.searchStream.next(target.value) | ||
71 | } | 71 | } |
72 | 72 | ||
73 | protected abstract loadData (): void | 73 | protected abstract loadData (): void |
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts index 16bb6d82c..5bd2b5e43 100644 --- a/client/src/app/shared/rest/rest.service.ts +++ b/client/src/app/shared/rest/rest.service.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { HttpParams } from '@angular/common/http' | 2 | import { HttpParams } from '@angular/common/http' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/api' |
4 | import { ComponentPagination, ComponentPaginationLight } from './component-pagination.model' | 4 | import { ComponentPagination, ComponentPaginationLight } from './component-pagination.model' |
5 | 5 | ||
6 | import { RestPagination } from './rest-pagination' | 6 | import { RestPagination } from './rest-pagination' |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index b2eb13f73..30b3ba0c1 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -5,9 +5,10 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' | |||
5 | import { RouterModule } from '@angular/router' | 5 | import { RouterModule } from '@angular/router' |
6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' | 6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' |
7 | import { HelpComponent } from '@app/shared/misc/help.component' | 7 | import { HelpComponent } from '@app/shared/misc/help.component' |
8 | import { ListOverflowComponent } from '@app/shared/misc/list-overflow.component' | ||
8 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | 9 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' |
9 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' | 10 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' |
10 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' | 11 | import { SharedModule as PrimeSharedModule } from 'primeng/api' |
11 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | 12 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' |
12 | import { ButtonComponent } from './buttons/button.component' | 13 | import { ButtonComponent } from './buttons/button.component' |
13 | import { DeleteButtonComponent } from './buttons/delete-button.component' | 14 | import { DeleteButtonComponent } from './buttons/delete-button.component' |
@@ -88,16 +89,18 @@ import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe' | |||
88 | import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe' | 89 | import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe' |
89 | import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' | 90 | import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' |
90 | import { FromNowPipe } from '@app/shared/angular/from-now.pipe' | 91 | import { FromNowPipe } from '@app/shared/angular/from-now.pipe' |
92 | import { HighlightPipe } from '@app/shared/angular/highlight.pipe' | ||
91 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' | 93 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' |
92 | import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component' | 94 | import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component' |
93 | import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' | 95 | import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' |
94 | import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' | 96 | import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' |
95 | import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' | 97 | import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' |
96 | import { ClipboardModule } from 'ngx-clipboard' | ||
97 | import { FollowService } from '@app/shared/instance/follow.service' | 98 | import { FollowService } from '@app/shared/instance/follow.service' |
98 | import { MultiSelectModule } from 'primeng/multiselect' | 99 | import { MultiSelectModule } from 'primeng/multiselect' |
99 | import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' | 100 | import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' |
100 | import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' | 101 | import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' |
102 | import { RedundancyService } from '@app/shared/video/redundancy.service' | ||
103 | import { ClipboardModule } from '@angular/cdk/clipboard' | ||
101 | 104 | ||
102 | @NgModule({ | 105 | @NgModule({ |
103 | imports: [ | 106 | imports: [ |
@@ -147,6 +150,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop | |||
147 | NumberFormatterPipe, | 150 | NumberFormatterPipe, |
148 | ObjectLengthPipe, | 151 | ObjectLengthPipe, |
149 | FromNowPipe, | 152 | FromNowPipe, |
153 | HighlightPipe, | ||
150 | PeerTubeTemplateDirective, | 154 | PeerTubeTemplateDirective, |
151 | VideoDurationPipe, | 155 | VideoDurationPipe, |
152 | 156 | ||
@@ -155,6 +159,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop | |||
155 | InfiniteScrollerDirective, | 159 | InfiniteScrollerDirective, |
156 | TextareaAutoResizeDirective, | 160 | TextareaAutoResizeDirective, |
157 | HelpComponent, | 161 | HelpComponent, |
162 | ListOverflowComponent, | ||
158 | 163 | ||
159 | ReactiveFileComponent, | 164 | ReactiveFileComponent, |
160 | PeertubeCheckboxComponent, | 165 | PeertubeCheckboxComponent, |
@@ -226,6 +231,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop | |||
226 | InfiniteScrollerDirective, | 231 | InfiniteScrollerDirective, |
227 | TextareaAutoResizeDirective, | 232 | TextareaAutoResizeDirective, |
228 | HelpComponent, | 233 | HelpComponent, |
234 | ListOverflowComponent, | ||
229 | InputReadonlyCopyComponent, | 235 | InputReadonlyCopyComponent, |
230 | 236 | ||
231 | ReactiveFileComponent, | 237 | ReactiveFileComponent, |
@@ -250,6 +256,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop | |||
250 | NumberFormatterPipe, | 256 | NumberFormatterPipe, |
251 | ObjectLengthPipe, | 257 | ObjectLengthPipe, |
252 | FromNowPipe, | 258 | FromNowPipe, |
259 | HighlightPipe, | ||
253 | PeerTubeTemplateDirective, | 260 | PeerTubeTemplateDirective, |
254 | VideoDurationPipe | 261 | VideoDurationPipe |
255 | ], | 262 | ], |
@@ -300,6 +307,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop | |||
300 | UserNotificationService, | 307 | UserNotificationService, |
301 | 308 | ||
302 | FollowService, | 309 | FollowService, |
310 | RedundancyService, | ||
303 | 311 | ||
304 | I18n | 312 | I18n |
305 | ] | 313 | ] |
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.html b/client/src/app/shared/user-subscription/subscribe-button.component.html index f08c88f3c..85b3d1fdb 100644 --- a/client/src/app/shared/user-subscription/subscribe-button.component.html +++ b/client/src/app/shared/user-subscription/subscribe-button.component.html | |||
@@ -55,7 +55,7 @@ | |||
55 | </button> | 55 | </button> |
56 | 56 | ||
57 | <button class="dropdown-item dropdown-item-neutral" i18n>Subscribe with a Mastodon account:</button> | 57 | <button class="dropdown-item dropdown-item-neutral" i18n>Subscribe with a Mastodon account:</button> |
58 | <my-remote-subscribe showHelp="true" [uri]="uri"></my-remote-subscribe> | 58 | <my-remote-subscribe [showHelp]="true" [uri]="uri"></my-remote-subscribe> |
59 | 59 | ||
60 | <div class="dropdown-divider"></div> | 60 | <div class="dropdown-divider"></div> |
61 | 61 | ||
diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts index b0b59ea0c..61a328575 100644 --- a/client/src/app/shared/video-abuse/video-abuse.service.ts +++ b/client/src/app/shared/video-abuse/video-abuse.service.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { catchError, map } from 'rxjs/operators' | 1 | import { catchError, map } from 'rxjs/operators' |
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { SortMeta } from 'primeng/components/common/sortmeta' | 4 | import { SortMeta } from 'primeng/api' |
5 | import { Observable } from 'rxjs' | 5 | import { Observable } from 'rxjs' |
6 | import { ResultList, VideoAbuse, VideoAbuseUpdate } from '../../../../../shared' | 6 | import { ResultList, VideoAbuse, VideoAbuseUpdate } from '../../../../../shared' |
7 | import { environment } from '../../../environments/environment' | 7 | import { environment } from '../../../environments/environment' |
diff --git a/client/src/app/shared/video-blacklist/video-blacklist.service.ts b/client/src/app/shared/video-blacklist/video-blacklist.service.ts index 491fa698b..116177c4a 100644 --- a/client/src/app/shared/video-blacklist/video-blacklist.service.ts +++ b/client/src/app/shared/video-blacklist/video-blacklist.service.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { catchError, map, concatMap, toArray } from 'rxjs/operators' | 1 | import { catchError, map, concatMap, toArray } from 'rxjs/operators' |
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { SortMeta } from 'primeng/components/common/sortmeta' | 4 | import { SortMeta } from 'primeng/api' |
5 | import { from as observableFrom, Observable } from 'rxjs' | 5 | import { from as observableFrom, Observable } from 'rxjs' |
6 | import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared' | 6 | import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared' |
7 | import { Video } from '../video/video.model' | 7 | import { Video } from '../video/video.model' |
diff --git a/client/src/app/shared/video-import/video-import.service.ts b/client/src/app/shared/video-import/video-import.service.ts index 3e3fb7dfb..afd9e3fb5 100644 --- a/client/src/app/shared/video-import/video-import.service.ts +++ b/client/src/app/shared/video-import/video-import.service.ts | |||
@@ -9,7 +9,7 @@ import { VideoImportCreate, VideoUpdate } from '../../../../../shared/models/vid | |||
9 | import { objectToFormData } from '@app/shared/misc/utils' | 9 | import { objectToFormData } from '@app/shared/misc/utils' |
10 | import { ResultList } from '../../../../../shared/models/result-list.model' | 10 | import { ResultList } from '../../../../../shared/models/result-list.model' |
11 | import { UserService } from '@app/shared/users/user.service' | 11 | import { UserService } from '@app/shared/users/user.service' |
12 | import { SortMeta } from 'primeng/components/common/sortmeta' | 12 | import { SortMeta } from 'primeng/api' |
13 | import { RestPagination } from '@app/shared/rest' | 13 | import { RestPagination } from '@app/shared/rest' |
14 | import { ServerService } from '@app/core' | 14 | import { ServerService } from '@app/core' |
15 | 15 | ||
diff --git a/client/src/app/shared/video-ownership/video-ownership.service.ts b/client/src/app/shared/video-ownership/video-ownership.service.ts index aa9e4839a..b95d5b792 100644 --- a/client/src/app/shared/video-ownership/video-ownership.service.ts +++ b/client/src/app/shared/video-ownership/video-ownership.service.ts | |||
@@ -5,7 +5,7 @@ import { environment } from '../../../environments/environment' | |||
5 | import { RestExtractor, RestService } from '../rest' | 5 | import { RestExtractor, RestService } from '../rest' |
6 | import { VideoChangeOwnershipCreate } from '../../../../../shared/models/videos' | 6 | import { VideoChangeOwnershipCreate } from '../../../../../shared/models/videos' |
7 | import { Observable } from 'rxjs/index' | 7 | import { Observable } from 'rxjs/index' |
8 | import { SortMeta } from 'primeng/components/common/sortmeta' | 8 | import { SortMeta } from 'primeng/api' |
9 | import { ResultList, VideoChangeOwnership } from '../../../../../shared' | 9 | import { ResultList, VideoChangeOwnership } from '../../../../../shared' |
10 | import { RestPagination } from '@app/shared/rest' | 10 | import { RestPagination } from '@app/shared/rest' |
11 | import { VideoChangeOwnershipAccept } from '../../../../../shared/models/videos/video-change-ownership-accept.model' | 11 | import { VideoChangeOwnershipAccept } from '../../../../../shared/models/videos/video-change-ownership-accept.model' |
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts index 4864581b5..a2c0724cd 100644 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts +++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts | |||
@@ -18,7 +18,7 @@ import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist- | |||
18 | changeDetection: ChangeDetectionStrategy.OnPush | 18 | changeDetection: ChangeDetectionStrategy.OnPush |
19 | }) | 19 | }) |
20 | export class VideoPlaylistElementMiniatureComponent implements OnInit { | 20 | export class VideoPlaylistElementMiniatureComponent implements OnInit { |
21 | @ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown | 21 | @ViewChild('moreDropdown') moreDropdown: NgbDropdown |
22 | 22 | ||
23 | @Input() playlist: VideoPlaylist | 23 | @Input() playlist: VideoPlaylist |
24 | @Input() playlistElement: VideoPlaylistElement | 24 | @Input() playlistElement: VideoPlaylistElement |
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index c2fe6f754..2f5f82aa3 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -14,6 +14,7 @@ import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | |||
14 | import { I18n } from '@ngx-translate/i18n-polyfill' | 14 | import { I18n } from '@ngx-translate/i18n-polyfill' |
15 | import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' | 15 | import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' |
16 | import { ServerConfig } from '@shared/models' | 16 | import { ServerConfig } from '@shared/models' |
17 | import { GlobalIconName } from '@app/shared/images/global-icon.component' | ||
17 | 18 | ||
18 | enum GroupDate { | 19 | enum GroupDate { |
19 | UNKNOWN = 0, | 20 | UNKNOWN = 0, |
@@ -61,7 +62,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
61 | 62 | ||
62 | actions: { | 63 | actions: { |
63 | routerLink: string | 64 | routerLink: string |
64 | iconName: string | 65 | iconName: GlobalIconName |
65 | label: string | 66 | label: string |
66 | }[] = [] | 67 | }[] = [] |
67 | 68 | ||
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/modals/video-blacklist.component.ts b/client/src/app/shared/video/modals/video-blacklist.component.ts index f0c70a365..6ef9c250b 100644 --- a/client/src/app/shared/video/modals/video-blacklist.component.ts +++ b/client/src/app/shared/video/modals/video-blacklist.component.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier, RedirectService } from '@app/core' | 2 | import { Notifier, RedirectService } from '@app/core' |
3 | import { VideoBlacklistService } from '../../../shared/video-blacklist' | 3 | import { VideoBlacklistService } from '../../../shared/video-blacklist' |
4 | import { VideoDetails } from '../../../shared/video/video-details.model' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 5 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
9 | import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms' | 8 | import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms' |
9 | import { Video } from '@app/shared/video/video.model' | ||
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
12 | selector: 'my-video-blacklist', | 12 | selector: 'my-video-blacklist', |
@@ -14,7 +14,7 @@ import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms | |||
14 | styleUrls: [ './video-blacklist.component.scss' ] | 14 | styleUrls: [ './video-blacklist.component.scss' ] |
15 | }) | 15 | }) |
16 | export class VideoBlacklistComponent extends FormReactive implements OnInit { | 16 | export class VideoBlacklistComponent extends FormReactive implements OnInit { |
17 | @Input() video: VideoDetails = null | 17 | @Input() video: Video = null |
18 | 18 | ||
19 | @ViewChild('modal', { static: true }) modal: NgbModal | 19 | @ViewChild('modal', { static: true }) modal: NgbModal |
20 | 20 | ||
@@ -46,7 +46,7 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit { | |||
46 | } | 46 | } |
47 | 47 | ||
48 | show () { | 48 | show () { |
49 | this.openedModal = this.modalService.open(this.modal, { keyboard: false }) | 49 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) |
50 | } | 50 | } |
51 | 51 | ||
52 | hide () { | 52 | hide () { |
diff --git a/client/src/app/shared/video/modals/video-download.component.html b/client/src/app/shared/video/modals/video-download.component.html index 8cca985b1..976da03f3 100644 --- a/client/src/app/shared/video/modals/video-download.component.html +++ b/client/src/app/shared/video/modals/video-download.component.html | |||
@@ -23,13 +23,15 @@ | |||
23 | <select *ngIf="type === 'video'" [(ngModel)]="resolutionId"> | 23 | <select *ngIf="type === 'video'" [(ngModel)]="resolutionId"> |
24 | <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option> | 24 | <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option> |
25 | </select> | 25 | </select> |
26 | |||
26 | <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId"> | 27 | <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId"> |
27 | <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option> | 28 | <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option> |
28 | </select> | 29 | </select> |
29 | </div> | 30 | </div> |
31 | |||
30 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> | 32 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> |
31 | <div class="input-group-append"> | 33 | <div class="input-group-append"> |
32 | <button [ngxClipboard]="urlInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | 34 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> |
33 | <span class="glyphicon glyphicon-copy"></span> | 35 | <span class="glyphicon glyphicon-copy"></span> |
34 | </button> | 36 | </button> |
35 | </div> | 37 | </div> |
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts index c1ceca263..6909c4279 100644 --- a/client/src/app/shared/video/modals/video-download.component.ts +++ b/client/src/app/shared/video/modals/video-download.component.ts | |||
@@ -48,7 +48,7 @@ export class VideoDownloadComponent { | |||
48 | this.video = video | 48 | this.video = video |
49 | this.videoCaptions = videoCaptions && videoCaptions.length ? videoCaptions : undefined | 49 | this.videoCaptions = videoCaptions && videoCaptions.length ? videoCaptions : undefined |
50 | 50 | ||
51 | this.activeModal = this.modalService.open(this.modal) | 51 | this.activeModal = this.modalService.open(this.modal, { centered: true }) |
52 | 52 | ||
53 | this.resolutionId = this.getVideoFiles()[0].resolution.id | 53 | this.resolutionId = this.getVideoFiles()[0].resolution.id |
54 | if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id | 54 | if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id |
diff --git a/client/src/app/shared/video/modals/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts index 1d368ff17..988fa03d4 100644 --- a/client/src/app/shared/video/modals/video-report.component.ts +++ b/client/src/app/shared/video/modals/video-report.component.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { FormReactive } from '../../../shared/forms' | 3 | import { FormReactive } from '../../../shared/forms' |
4 | import { VideoDetails } from '../../../shared/video/video-details.model' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 5 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
7 | import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' | 6 | import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' |
8 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
9 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
10 | import { VideoAbuseService } from '@app/shared/video-abuse' | 9 | import { VideoAbuseService } from '@app/shared/video-abuse' |
10 | import { Video } from '@app/shared/video/video.model' | ||
11 | 11 | ||
12 | @Component({ | 12 | @Component({ |
13 | selector: 'my-video-report', | 13 | selector: 'my-video-report', |
@@ -15,7 +15,7 @@ import { VideoAbuseService } from '@app/shared/video-abuse' | |||
15 | styleUrls: [ './video-report.component.scss' ] | 15 | styleUrls: [ './video-report.component.scss' ] |
16 | }) | 16 | }) |
17 | export class VideoReportComponent extends FormReactive implements OnInit { | 17 | export class VideoReportComponent extends FormReactive implements OnInit { |
18 | @Input() video: VideoDetails = null | 18 | @Input() video: Video = null |
19 | 19 | ||
20 | @ViewChild('modal', { static: true }) modal: NgbModal | 20 | @ViewChild('modal', { static: true }) modal: NgbModal |
21 | 21 | ||
@@ -53,7 +53,7 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
53 | } | 53 | } |
54 | 54 | ||
55 | show () { | 55 | show () { |
56 | this.openedModal = this.modalService.open(this.modal, { keyboard: false }) | 56 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) |
57 | } | 57 | } |
58 | 58 | ||
59 | hide () { | 59 | hide () { |
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..69f45346e 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({ |
@@ -30,12 +32,12 @@ export type VideoActionsDisplayType = { | |||
30 | styleUrls: [ './video-actions-dropdown.component.scss' ] | 32 | styleUrls: [ './video-actions-dropdown.component.scss' ] |
31 | }) | 33 | }) |
32 | export class VideoActionsDropdownComponent implements OnChanges { | 34 | export class VideoActionsDropdownComponent implements OnChanges { |
33 | @ViewChild('playlistDropdown', { static: false }) playlistDropdown: NgbDropdown | 35 | @ViewChild('playlistDropdown') playlistDropdown: NgbDropdown |
34 | @ViewChild('playlistAdd', { static: false }) playlistAdd: VideoAddToPlaylistComponent | 36 | @ViewChild('playlistAdd') playlistAdd: VideoAddToPlaylistComponent |
35 | 37 | ||
36 | @ViewChild('videoDownloadModal', { static: false }) videoDownloadModal: VideoDownloadComponent | 38 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent |
37 | @ViewChild('videoReportModal', { static: false }) videoReportModal: VideoReportComponent | 39 | @ViewChild('videoReportModal') videoReportModal: VideoReportComponent |
38 | @ViewChild('videoBlacklistModal', { static: false }) videoBlacklistModal: VideoBlacklistComponent | 40 | @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent |
39 | 41 | ||
40 | @Input() video: Video | VideoDetails | 42 | @Input() video: Video | VideoDetails |
41 | @Input() videoCaptions: VideoCaption[] = [] | 43 | @Input() videoCaptions: VideoCaption[] = [] |
@@ -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-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts index 2420ec715..111b4c8bb 100644 --- a/client/src/app/shared/video/video-thumbnail.component.ts +++ b/client/src/app/shared/video/video-thumbnail.component.ts | |||
@@ -12,7 +12,7 @@ export class VideoThumbnailComponent { | |||
12 | @Input() video: Video | 12 | @Input() video: Video |
13 | @Input() nsfw = false | 13 | @Input() nsfw = false |
14 | @Input() routerLink: any[] | 14 | @Input() routerLink: any[] |
15 | @Input() queryParams: any[] | 15 | @Input() queryParams: { [ p: string ]: any } |
16 | 16 | ||
17 | @Input() displayWatchLaterPlaylist: boolean | 17 | @Input() displayWatchLaterPlaylist: boolean |
18 | @Input() inWatchLaterPlaylist: boolean | 18 | @Input() inWatchLaterPlaylist: boolean |
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-edit/shared/video-caption-add-modal.component.ts b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts index 1a9bf5171..9856aac9e 100644 --- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts | |||
@@ -56,7 +56,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni | |||
56 | show () { | 56 | show () { |
57 | this.closingModal = false | 57 | this.closingModal = false |
58 | 58 | ||
59 | this.openedModal = this.modalService.open(this.modal, { keyboard: false }) | 59 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) |
60 | } | 60 | } |
61 | 61 | ||
62 | hide () { | 62 | hide () { |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index e40649d95..6d72e5765 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html | |||
@@ -29,7 +29,7 @@ | |||
29 | <tag-input | 29 | <tag-input |
30 | [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | 30 | [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" |
31 | i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag" | 31 | i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag" |
32 | formControlName="tags" maxItems="5" modelAsStrings="true" | 32 | formControlName="tags" [maxItems]="5" [modelAsStrings]="true" |
33 | ></tag-input> | 33 | ></tag-input> |
34 | </div> | 34 | </div> |
35 | 35 | ||
@@ -44,7 +44,7 @@ | |||
44 | </ng-template> | 44 | </ng-template> |
45 | </my-help> | 45 | </my-help> |
46 | 46 | ||
47 | <my-markdown-textarea truncate="250" formControlName="description" markdownVideo="true"></my-markdown-textarea> | 47 | <my-markdown-textarea [truncate]="250" formControlName="description" [markdownVideo]="true"></my-markdown-textarea> |
48 | 48 | ||
49 | <div *ngIf="formErrors.description" class="form-error"> | 49 | <div *ngIf="formErrors.description" class="form-error"> |
50 | {{ formErrors.description }} | 50 | {{ formErrors.description }} |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.module.ts b/client/src/app/videos/+video-edit/shared/video-edit.module.ts index 39b6daa93..1357d607c 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.module.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.module.ts | |||
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core' | |||
2 | import { TagInputModule } from 'ngx-chips' | 2 | import { TagInputModule } from 'ngx-chips' |
3 | import { SharedModule } from '../../../shared/' | 3 | import { SharedModule } from '../../../shared/' |
4 | import { VideoEditComponent } from './video-edit.component' | 4 | import { VideoEditComponent } from './video-edit.component' |
5 | import { CalendarModule } from 'primeng/components/calendar/calendar' | 5 | import { CalendarModule } from 'primeng/calendar' |
6 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' | 6 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' |
7 | 7 | ||
8 | @NgModule({ | 8 | @NgModule({ |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts index 74e1e755b..e47624dd6 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts | |||
@@ -25,7 +25,7 @@ import { scrollToTop } from '@app/shared/misc/utils' | |||
25 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 25 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { |
26 | @Output() firstStepDone = new EventEmitter<string>() | 26 | @Output() firstStepDone = new EventEmitter<string>() |
27 | @Output() firstStepError = new EventEmitter<void>() | 27 | @Output() firstStepError = new EventEmitter<void>() |
28 | @ViewChild('torrentfileInput', { static: false }) torrentfileInput: ElementRef<HTMLInputElement> | 28 | @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> |
29 | 29 | ||
30 | magnetUri = '' | 30 | magnetUri = '' |
31 | 31 | ||
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts index aa87f9581..efdd284e8 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts | |||
@@ -27,7 +27,7 @@ import { scrollToTop } from '@app/shared/misc/utils' | |||
27 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { | 27 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { |
28 | @Output() firstStepDone = new EventEmitter<string>() | 28 | @Output() firstStepDone = new EventEmitter<string>() |
29 | @Output() firstStepError = new EventEmitter<void>() | 29 | @Output() firstStepError = new EventEmitter<void>() |
30 | @ViewChild('videofileInput', { static: false }) videofileInput: ElementRef<HTMLInputElement> | 30 | @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> |
31 | 31 | ||
32 | // So that it can be accessed in the template | 32 | // So that it can be accessed in the template |
33 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY | 33 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY |
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 401d8a08f..30ab08ea0 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts | |||
@@ -12,9 +12,9 @@ import { ServerConfig } from '@shared/models' | |||
12 | styleUrls: [ './video-add.component.scss' ] | 12 | styleUrls: [ './video-add.component.scss' ] |
13 | }) | 13 | }) |
14 | export class VideoAddComponent implements OnInit, CanComponentDeactivate { | 14 | export class VideoAddComponent implements OnInit, CanComponentDeactivate { |
15 | @ViewChild('videoUpload', { static: false }) videoUpload: VideoUploadComponent | 15 | @ViewChild('videoUpload') videoUpload: VideoUploadComponent |
16 | @ViewChild('videoImportUrl', { static: false }) videoImportUrl: VideoImportUrlComponent | 16 | @ViewChild('videoImportUrl') videoImportUrl: VideoImportUrlComponent |
17 | @ViewChild('videoImportTorrent', { static: false }) videoImportTorrent: VideoImportTorrentComponent | 17 | @ViewChild('videoImportTorrent') videoImportTorrent: VideoImportTorrentComponent |
18 | 18 | ||
19 | secondStepType: 'upload' | 'import-url' | 'import-torrent' | 19 | secondStepType: 'upload' | 'import-url' | 'import-torrent' |
20 | videoName: string | 20 | videoName: string |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts index 1be96ad9e..a8c432653 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts | |||
@@ -25,7 +25,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit { | |||
25 | @Input() parentComments: VideoComment[] | 25 | @Input() parentComments: VideoComment[] |
26 | @Input() focusOnInit = false | 26 | @Input() focusOnInit = false |
27 | 27 | ||
28 | @Output() commentCreated = new EventEmitter<VideoCommentCreate>() | 28 | @Output() commentCreated = new EventEmitter<VideoComment>() |
29 | @Output() cancel = new EventEmitter() | 29 | @Output() cancel = new EventEmitter() |
30 | 30 | ||
31 | @ViewChild('visitorModal', { static: true }) visitorModal: NgbModal | 31 | @ViewChild('visitorModal', { static: true }) visitorModal: NgbModal |
@@ -96,7 +96,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit { | |||
96 | this.addingComment = true | 96 | this.addingComment = true |
97 | 97 | ||
98 | const commentCreate: VideoCommentCreate = this.form.value | 98 | const commentCreate: VideoCommentCreate = this.form.value |
99 | let obs: Observable<any> | 99 | let obs: Observable<VideoComment> |
100 | 100 | ||
101 | if (this.parentComment) { | 101 | if (this.parentComment) { |
102 | obs = this.addCommentReply(commentCreate) | 102 | obs = this.addCommentReply(commentCreate) |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts b/client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts new file mode 100644 index 000000000..1566d7369 --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import { VideoCommentThreadTree as VideoCommentThreadTreeServerModel } from '../../../../../../shared/models/videos/video-comment.model' | ||
2 | import { VideoComment } from '@app/videos/+video-watch/comment/video-comment.model' | ||
3 | |||
4 | export class VideoCommentThreadTree implements VideoCommentThreadTreeServerModel { | ||
5 | comment: VideoComment | ||
6 | children: VideoCommentThreadTree[] | ||
7 | } | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts index 61f9335d1..f7eca45fd 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' |
2 | import { User, UserRight } from '../../../../../../shared/models/users' | 2 | import { User, UserRight } from '../../../../../../shared/models/users' |
3 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' | ||
4 | import { AuthService } from '@app/core/auth' | 3 | import { AuthService } from '@app/core/auth' |
5 | import { AccountService } from '@app/shared/account/account.service' | 4 | import { AccountService } from '@app/shared/account/account.service' |
6 | import { Video } from '@app/shared/video/video.model' | 5 | import { Video } from '@app/shared/video/video.model' |
@@ -10,6 +9,7 @@ import { Account } from '@app/shared/account/account.model' | |||
10 | import { Notifier } from '@app/core' | 9 | import { Notifier } from '@app/core' |
11 | import { UserService } from '@app/shared' | 10 | import { UserService } from '@app/shared' |
12 | import { Actor } from '@app/shared/actor/actor.model' | 11 | import { Actor } from '@app/shared/actor/actor.model' |
12 | import { VideoCommentThreadTree } from '@app/videos/+video-watch/comment/video-comment-thread-tree.model' | ||
13 | 13 | ||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-video-comment', | 15 | selector: 'my-video-comment', |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.model.ts b/client/src/app/videos/+video-watch/comment/video-comment.model.ts index aaeb0ea9c..171fc4acc 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.model.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Account as AccountInterface } from '../../../../../../shared/models/actors' | 1 | import { Account as AccountInterface } from '../../../../../../shared/models/actors' |
2 | import { VideoComment as VideoCommentServerModel } from '../../../../../../shared/models/videos/video-comment.model' | 2 | import { VideoComment as VideoCommentServerModel, VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model' |
3 | import { Actor } from '@app/shared/actor/actor.model' | 3 | import { Actor } from '@app/shared/actor/actor.model' |
4 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | 4 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' |
5 | 5 | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts index a81e5236a..0b0715390 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts | |||
@@ -7,13 +7,14 @@ import { FeedFormat, ResultList } from '../../../../../../shared/models' | |||
7 | import { | 7 | import { |
8 | VideoComment as VideoCommentServerModel, | 8 | VideoComment as VideoCommentServerModel, |
9 | VideoCommentCreate, | 9 | VideoCommentCreate, |
10 | VideoCommentThreadTree | 10 | VideoCommentThreadTree as VideoCommentThreadTreeServerModel |
11 | } from '../../../../../../shared/models/videos/video-comment.model' | 11 | } from '../../../../../../shared/models/videos/video-comment.model' |
12 | import { environment } from '../../../../environments/environment' | 12 | import { environment } from '../../../../environments/environment' |
13 | import { RestExtractor, RestService } from '../../../shared/rest' | 13 | import { RestExtractor, RestService } from '../../../shared/rest' |
14 | import { ComponentPaginationLight } from '../../../shared/rest/component-pagination.model' | 14 | import { ComponentPaginationLight } from '../../../shared/rest/component-pagination.model' |
15 | import { CommentSortField } from '../../../shared/video/sort-field.type' | 15 | import { CommentSortField } from '../../../shared/video/sort-field.type' |
16 | import { VideoComment } from './video-comment.model' | 16 | import { VideoComment } from './video-comment.model' |
17 | import { VideoCommentThreadTree } from '@app/videos/+video-watch/comment/video-comment-thread-tree.model' | ||
17 | 18 | ||
18 | @Injectable() | 19 | @Injectable() |
19 | export class VideoCommentService { | 20 | export class VideoCommentService { |
@@ -76,9 +77,9 @@ export class VideoCommentService { | |||
76 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` | 77 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` |
77 | 78 | ||
78 | return this.authHttp | 79 | return this.authHttp |
79 | .get(url) | 80 | .get<VideoCommentThreadTreeServerModel>(url) |
80 | .pipe( | 81 | .pipe( |
81 | map(tree => this.extractVideoCommentTree(tree as VideoCommentThreadTree)), | 82 | map(tree => this.extractVideoCommentTree(tree)), |
82 | catchError(err => this.restExtractor.handleError(err)) | 83 | catchError(err => this.restExtractor.handleError(err)) |
83 | ) | 84 | ) |
84 | } | 85 | } |
@@ -138,12 +139,12 @@ export class VideoCommentService { | |||
138 | return { data: comments, total: totalComments } | 139 | return { data: comments, total: totalComments } |
139 | } | 140 | } |
140 | 141 | ||
141 | private extractVideoCommentTree (tree: VideoCommentThreadTree) { | 142 | private extractVideoCommentTree (tree: VideoCommentThreadTreeServerModel) { |
142 | if (!tree) return tree | 143 | if (!tree) return tree as VideoCommentThreadTree |
143 | 144 | ||
144 | tree.comment = new VideoComment(tree.comment) | 145 | tree.comment = new VideoComment(tree.comment) |
145 | tree.children.forEach(c => this.extractVideoCommentTree(c)) | 146 | tree.children.forEach(c => this.extractVideoCommentTree(c)) |
146 | 147 | ||
147 | return tree | 148 | return tree as VideoCommentThreadTree |
148 | } | 149 | } |
149 | } | 150 | } |
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts index 974c61d6c..750c09c47 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, Output, EventEmitter } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' |
2 | import { ActivatedRoute } from '@angular/router' | 2 | import { ActivatedRoute } from '@angular/router' |
3 | import { ConfirmService, Notifier } from '@app/core' | 3 | import { ConfirmService, Notifier } from '@app/core' |
4 | import { Subject, Subscription } from 'rxjs' | 4 | import { Subject, Subscription } from 'rxjs' |
5 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' | ||
6 | import { AuthService } from '../../../core/auth' | 5 | import { AuthService } from '../../../core/auth' |
7 | import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model' | 6 | import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model' |
8 | import { User } from '../../../shared/users' | 7 | import { User } from '../../../shared/users' |
@@ -13,6 +12,7 @@ import { VideoCommentService } from './video-comment.service' | |||
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | 12 | import { I18n } from '@ngx-translate/i18n-polyfill' |
14 | import { Syndication } from '@app/shared/video/syndication.model' | 13 | import { Syndication } from '@app/shared/video/syndication.model' |
15 | import { HooksService } from '@app/core/plugins/hooks.service' | 14 | import { HooksService } from '@app/core/plugins/hooks.service' |
15 | import { VideoCommentThreadTree } from '@app/videos/+video-watch/comment/video-comment-thread-tree.model' | ||
16 | 16 | ||
17 | @Component({ | 17 | @Component({ |
18 | selector: 'my-video-comments', | 18 | selector: 'my-video-comments', |
@@ -20,7 +20,7 @@ import { HooksService } from '@app/core/plugins/hooks.service' | |||
20 | styleUrls: ['./video-comments.component.scss'] | 20 | styleUrls: ['./video-comments.component.scss'] |
21 | }) | 21 | }) |
22 | export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | 22 | export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { |
23 | @ViewChild('commentHighlightBlock', { static: false }) commentHighlightBlock: ElementRef | 23 | @ViewChild('commentHighlightBlock') commentHighlightBlock: ElementRef |
24 | @Input() video: VideoDetails | 24 | @Input() video: VideoDetails |
25 | @Input() user: User | 25 | @Input() user: User |
26 | 26 | ||
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html index 549a9f30e..593dd8529 100644 --- a/client/src/app/videos/+video-watch/modal/video-share.component.html +++ b/client/src/app/videos/+video-watch/modal/video-share.component.html | |||
@@ -42,7 +42,7 @@ | |||
42 | <ngb-tab i18n-title title="QR-Code" id="qrcode"> | 42 | <ngb-tab i18n-title title="QR-Code" id="qrcode"> |
43 | <ng-template ngbTabContent> | 43 | <ng-template ngbTabContent> |
44 | <div class="tab-content"> | 44 | <div class="tab-content"> |
45 | <qrcode [qrdata]="getVideoUrl()" size="256" level="Q"></qrcode> | 45 | <qrcode [qrdata]="getVideoUrl()" [size]="256" level="Q"></qrcode> |
46 | </div> | 46 | </div> |
47 | </ng-template> | 47 | </ng-template> |
48 | </ngb-tab> | 48 | </ngb-tab> |
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts index a2b38b3a0..5109bcd11 100644 --- a/client/src/app/videos/+video-watch/modal/video-share.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-share.component.ts | |||
@@ -1,8 +1,6 @@ | |||
1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | ||
3 | import { VideoDetails } from '../../../shared/video/video-details.model' | 2 | import { VideoDetails } from '../../../shared/video/video-details.model' |
4 | import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' | 3 | import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
6 | import { NgbModal, NgbTabChangeEvent } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbModal, NgbTabChangeEvent } from '@ng-bootstrap/ng-bootstrap' |
7 | import { VideoCaption } from '@shared/models' | 5 | import { VideoCaption } from '@shared/models' |
8 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 6 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
@@ -74,7 +72,7 @@ export class VideoShareComponent { | |||
74 | controls: true | 72 | controls: true |
75 | } | 73 | } |
76 | 74 | ||
77 | this.modalService.open(this.modal) | 75 | this.modalService.open(this.modal, { centered: true }) |
78 | } | 76 | } |
79 | 77 | ||
80 | getVideoIframeCode () { | 78 | getVideoIframeCode () { |
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts index b56a51fbf..0058172f2 100644 --- a/client/src/app/videos/+video-watch/modal/video-support.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-support.component.ts | |||
@@ -21,7 +21,7 @@ export class VideoSupportComponent { | |||
21 | ) { } | 21 | ) { } |
22 | 22 | ||
23 | show () { | 23 | show () { |
24 | this.modalService.open(this.modal) | 24 | this.modalService.open(this.modal, { centered: true }) |
25 | 25 | ||
26 | this.markdownService.enhancedMarkdownToHTML(this.video.support) | 26 | this.markdownService.enhancedMarkdownToHTML(this.video.support) |
27 | .then(r => this.videoHTMLSupport = r) | 27 | .then(r => this.videoHTMLSupport = r) |
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> |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index e09e44809..ee3deb5e9 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -47,9 +47,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
47 | private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern' | 47 | private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern' |
48 | 48 | ||
49 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent | 49 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent |
50 | @ViewChild('videoShareModal', { static: false }) videoShareModal: VideoShareComponent | 50 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent |
51 | @ViewChild('videoSupportModal', { static: false }) videoSupportModal: VideoSupportComponent | 51 | @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent |
52 | @ViewChild('subscribeButton', { static: false }) subscribeButton: SubscribeButtonComponent | 52 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
53 | 53 | ||
54 | player: any | 54 | player: any |
55 | playerElement: HTMLVideoElement | 55 | playerElement: HTMLVideoElement |