diff options
Diffstat (limited to 'client/src/app')
114 files changed, 1602 insertions, 558 deletions
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html index f81465f88..6bc1d0448 100644 --- a/client/src/app/+about/about-follows/about-follows.component.html +++ b/client/src/app/+about/about-follows/about-follows.component.html | |||
@@ -9,7 +9,7 @@ | |||
9 | {{ follower}} | 9 | {{ follower}} |
10 | </a> | 10 | </a> |
11 | 11 | ||
12 | <button i18n class="showMore" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button> | 12 | <button i18n class="show-more" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button> |
13 | </div> | 13 | </div> |
14 | 14 | ||
15 | <div class="col-xl-6 col-md-12"> | 15 | <div class="col-xl-6 col-md-12"> |
diff --git a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts index c45269be4..dd774a4ef 100644 --- a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts +++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts | |||
@@ -14,6 +14,6 @@ export class AboutPeertubeContributorsComponent implements OnInit { | |||
14 | constructor (private markdownService: MarkdownService) { } | 14 | constructor (private markdownService: MarkdownService) { } |
15 | 15 | ||
16 | async ngOnInit () { | 16 | async ngOnInit () { |
17 | this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown) | 17 | this.creditsHtml = await this.markdownService.unsafeMarkdownToHTML(this.markdown, true) |
18 | } | 18 | } |
19 | } | 19 | } |
diff --git a/client/src/app/+about/about-routing.module.ts b/client/src/app/+about/about-routing.module.ts index 96a737555..880bf4a39 100644 --- a/client/src/app/+about/about-routing.module.ts +++ b/client/src/app/+about/about-routing.module.ts | |||
@@ -1,17 +1,15 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { AboutComponent } from './about.component' | ||
5 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' | ||
6 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' | ||
7 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' | 3 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' |
4 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' | ||
8 | import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver' | 5 | import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver' |
6 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' | ||
7 | import { AboutComponent } from './about.component' | ||
9 | 8 | ||
10 | const aboutRoutes: Routes = [ | 9 | const aboutRoutes: Routes = [ |
11 | { | 10 | { |
12 | path: '', | 11 | path: '', |
13 | component: AboutComponent, | 12 | component: AboutComponent, |
14 | canActivateChild: [ MetaGuard ], | ||
15 | children: [ | 13 | children: [ |
16 | { | 14 | { |
17 | path: '', | 15 | path: '', |
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss index f9d097644..2dfb057e7 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss | |||
@@ -36,6 +36,8 @@ | |||
36 | } | 36 | } |
37 | 37 | ||
38 | a { | 38 | a { |
39 | @include peertube-word-wrap; | ||
40 | |||
39 | color: pvar(--mainForegroundColor); | 41 | color: pvar(--mainForegroundColor); |
40 | } | 42 | } |
41 | 43 | ||
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts index 96d1e0f85..e146a5cd2 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts | |||
@@ -79,7 +79,13 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
79 | } | 79 | } |
80 | 80 | ||
81 | loadMoreChannels () { | 81 | loadMoreChannels () { |
82 | this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination) | 82 | const options = { |
83 | account: this.account, | ||
84 | componentPagination: this.channelPagination, | ||
85 | sort: '-updatedAt' | ||
86 | } | ||
87 | |||
88 | this.videoChannelService.listAccountVideoChannels(options) | ||
83 | .pipe( | 89 | .pipe( |
84 | tap(res => this.channelPagination.totalItems = res.total), | 90 | tap(res => this.channelPagination.totalItems = res.total), |
85 | switchMap(res => from(res.data)), | 91 | switchMap(res => from(res.data)), |
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts index 3bf0f7185..2f3792a8d 100644 --- a/client/src/app/+accounts/accounts-routing.module.ts +++ b/client/src/app/+accounts/accounts-routing.module.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { AccountSearchComponent } from './account-search/account-search.component' | 3 | import { AccountSearchComponent } from './account-search/account-search.component' |
5 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | 4 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' |
6 | import { AccountVideosComponent } from './account-videos/account-videos.component' | 5 | import { AccountVideosComponent } from './account-videos/account-videos.component' |
@@ -14,7 +13,6 @@ const accountsRoutes: Routes = [ | |||
14 | { | 13 | { |
15 | path: ':accountId', | 14 | path: ':accountId', |
16 | component: AccountsComponent, | 15 | component: AccountsComponent, |
17 | canActivateChild: [ MetaGuard ], | ||
18 | children: [ | 16 | children: [ |
19 | { | 17 | { |
20 | path: '', | 18 | path: '', |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index fbd7380a9..c69b04a01 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -66,7 +66,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
66 | distinctUntilChanged(), | 66 | distinctUntilChanged(), |
67 | switchMap(accountId => this.accountService.getAccount(accountId)), | 67 | switchMap(accountId => this.accountService.getAccount(accountId)), |
68 | tap(account => this.onAccount(account)), | 68 | tap(account => this.onAccount(account)), |
69 | switchMap(account => this.videoChannelService.listAccountVideoChannels(account)), | 69 | switchMap(account => this.videoChannelService.listAccountVideoChannels({ account })), |
70 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [ | 70 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [ |
71 | HttpStatusCode.BAD_REQUEST_400, | 71 | HttpStatusCode.BAD_REQUEST_400, |
72 | HttpStatusCode.NOT_FOUND_404 | 72 | HttpStatusCode.NOT_FOUND_404 |
diff --git a/client/src/app/+admin/admin-routing.module.ts b/client/src/app/+admin/admin-routing.module.ts index 986dae8eb..d029661d3 100644 --- a/client/src/app/+admin/admin-routing.module.ts +++ b/client/src/app/+admin/admin-routing.module.ts | |||
@@ -4,7 +4,6 @@ import { ConfigRoutes } from '@app/+admin/config' | |||
4 | import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes' | 4 | import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes' |
5 | import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes' | 5 | import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes' |
6 | import { SystemRoutes } from '@app/+admin/system' | 6 | import { SystemRoutes } from '@app/+admin/system' |
7 | import { MetaGuard } from '@ngx-meta/core' | ||
8 | import { AdminComponent } from './admin.component' | 7 | import { AdminComponent } from './admin.component' |
9 | import { FollowsRoutes } from './follows' | 8 | import { FollowsRoutes } from './follows' |
10 | import { UsersRoutes } from './users' | 9 | import { UsersRoutes } from './users' |
@@ -13,8 +12,6 @@ const adminRoutes: Routes = [ | |||
13 | { | 12 | { |
14 | path: '', | 13 | path: '', |
15 | component: AdminComponent, | 14 | component: AdminComponent, |
16 | canActivate: [ MetaGuard ], | ||
17 | canActivateChild: [ MetaGuard ], | ||
18 | children: [ | 15 | children: [ |
19 | { | 16 | { |
20 | path: '', | 17 | path: '', |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 45366f9ec..a7fe20b07 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -4,12 +4,13 @@ import { TableModule } from 'primeng/table' | |||
4 | import { NgModule } from '@angular/core' | 4 | import { NgModule } from '@angular/core' |
5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' | 5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' |
6 | import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit' | 6 | import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit' |
7 | import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module' | ||
8 | import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup' | ||
7 | import { SharedFormModule } from '@app/shared/shared-forms' | 9 | import { SharedFormModule } from '@app/shared/shared-forms' |
8 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 10 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
9 | import { SharedMainModule } from '@app/shared/shared-main' | 11 | import { SharedMainModule } from '@app/shared/shared-main' |
10 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 12 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
11 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' | 13 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' |
12 | import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' | ||
13 | import { AdminRoutingModule } from './admin-routing.module' | 14 | import { AdminRoutingModule } from './admin-routing.module' |
14 | import { AdminComponent } from './admin.component' | 15 | import { AdminComponent } from './admin.component' |
15 | import { | 16 | import { |
@@ -18,6 +19,7 @@ import { | |||
18 | EditBasicConfigurationComponent, | 19 | EditBasicConfigurationComponent, |
19 | EditConfigurationService, | 20 | EditConfigurationService, |
20 | EditCustomConfigComponent, | 21 | EditCustomConfigComponent, |
22 | EditHomepageComponent, | ||
21 | EditInstanceInformationComponent, | 23 | EditInstanceInformationComponent, |
22 | EditLiveConfigurationComponent, | 24 | EditLiveConfigurationComponent, |
23 | EditVODTranscodingComponent | 25 | EditVODTranscodingComponent |
@@ -53,6 +55,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom | |||
53 | SharedVideoCommentModule, | 55 | SharedVideoCommentModule, |
54 | SharedActorImageModule, | 56 | SharedActorImageModule, |
55 | SharedActorImageEditModule, | 57 | SharedActorImageEditModule, |
58 | SharedCustomMarkupModule, | ||
56 | 59 | ||
57 | TableModule, | 60 | TableModule, |
58 | SelectButtonModule, | 61 | SelectButtonModule, |
@@ -100,7 +103,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom | |||
100 | EditVODTranscodingComponent, | 103 | EditVODTranscodingComponent, |
101 | EditLiveConfigurationComponent, | 104 | EditLiveConfigurationComponent, |
102 | EditAdvancedConfigurationComponent, | 105 | EditAdvancedConfigurationComponent, |
103 | EditInstanceInformationComponent | 106 | EditInstanceInformationComponent, |
107 | EditHomepageComponent | ||
104 | ], | 108 | ], |
105 | 109 | ||
106 | exports: [ | 110 | exports: [ |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html index 84a793ae4..451e6a34a 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html | |||
@@ -26,22 +26,13 @@ | |||
26 | <div class="form-group" formGroupName="instance"> | 26 | <div class="form-group" formGroupName="instance"> |
27 | <label i18n for="instanceDefaultClientRoute">Landing page</label> | 27 | <label i18n for="instanceDefaultClientRoute">Landing page</label> |
28 | 28 | ||
29 | <div class="peertube-select-container"> | 29 | <my-select-custom-value |
30 | <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control"> | 30 | id="instanceDefaultClientRoute" |
31 | <option i18n value="/videos/overview">Discover videos</option> | 31 | [items]="defaultLandingPageOptions" |
32 | 32 | formControlName="defaultClientRoute" | |
33 | <optgroup i18n-label label="Trending pages"> | 33 | inputType="text" |
34 | <option i18n value="/videos/trending">Default trending page</option> | 34 | [clearable]="false" |
35 | <option i18n value="/videos/trending?alg=best" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('best')">Best videos</option> | 35 | ></my-select-custom-value> |
36 | <option i18n value="/videos/trending?alg=hot" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('hot')">Hot videos</option> | ||
37 | <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-viewed')">Most viewed videos</option> | ||
38 | <option i18n value="/videos/trending?alg=most-liked" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-liked')">Most liked videos</option> | ||
39 | </optgroup> | ||
40 | |||
41 | <option i18n value="/videos/recently-added">Recently added videos</option> | ||
42 | <option i18n value="/videos/local">Local videos</option> | ||
43 | </select> | ||
44 | </div> | ||
45 | 36 | ||
46 | <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div> | 37 | <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div> |
47 | </div> | 38 | </div> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts index 34d05f9f3..d50148e7a 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | 1 | ||
2 | import { pairwise } from 'rxjs/operators' | 2 | import { pairwise } from 'rxjs/operators' |
3 | import { Component, Input, OnInit } from '@angular/core' | 3 | import { SelectOptionsItem } from 'src/types/select-options-item.model' |
4 | import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core' | ||
4 | import { FormGroup } from '@angular/forms' | 5 | import { FormGroup } from '@angular/forms' |
6 | import { MenuService } from '@app/core' | ||
5 | import { ServerConfig } from '@shared/models' | 7 | import { ServerConfig } from '@shared/models' |
6 | import { ConfigService } from '../shared/config.service' | 8 | import { ConfigService } from '../shared/config.service' |
7 | 9 | ||
@@ -10,22 +12,31 @@ import { ConfigService } from '../shared/config.service' | |||
10 | templateUrl: './edit-basic-configuration.component.html', | 12 | templateUrl: './edit-basic-configuration.component.html', |
11 | styleUrls: [ './edit-custom-config.component.scss' ] | 13 | styleUrls: [ './edit-custom-config.component.scss' ] |
12 | }) | 14 | }) |
13 | export class EditBasicConfigurationComponent implements OnInit { | 15 | export class EditBasicConfigurationComponent implements OnInit, OnChanges { |
14 | @Input() form: FormGroup | 16 | @Input() form: FormGroup |
15 | @Input() formErrors: any | 17 | @Input() formErrors: any |
16 | 18 | ||
17 | @Input() serverConfig: ServerConfig | 19 | @Input() serverConfig: ServerConfig |
18 | 20 | ||
19 | signupAlertMessage: string | 21 | signupAlertMessage: string |
22 | defaultLandingPageOptions: SelectOptionsItem[] = [] | ||
20 | 23 | ||
21 | constructor ( | 24 | constructor ( |
22 | private configService: ConfigService | 25 | private configService: ConfigService, |
26 | private menuService: MenuService | ||
23 | ) { } | 27 | ) { } |
24 | 28 | ||
25 | ngOnInit () { | 29 | ngOnInit () { |
30 | this.buildLandingPageOptions() | ||
26 | this.checkSignupField() | 31 | this.checkSignupField() |
27 | } | 32 | } |
28 | 33 | ||
34 | ngOnChanges (changes: SimpleChanges) { | ||
35 | if (changes['serverConfig']) { | ||
36 | this.buildLandingPageOptions() | ||
37 | } | ||
38 | } | ||
39 | |||
29 | getVideoQuotaOptions () { | 40 | getVideoQuotaOptions () { |
30 | return this.configService.videoQuotaOptions | 41 | return this.configService.videoQuotaOptions |
31 | } | 42 | } |
@@ -70,6 +81,15 @@ export class EditBasicConfigurationComponent implements OnInit { | |||
70 | return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true | 81 | return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true |
71 | } | 82 | } |
72 | 83 | ||
84 | buildLandingPageOptions () { | ||
85 | this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig) | ||
86 | .map(o => ({ | ||
87 | id: o.path, | ||
88 | label: o.label, | ||
89 | description: o.path | ||
90 | })) | ||
91 | } | ||
92 | |||
73 | private checkSignupField () { | 93 | private checkSignupField () { |
74 | const signupControl = this.form.get('signup.enabled') | 94 | const signupControl = this.form.get('signup.enabled') |
75 | 95 | ||
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 b6365614d..3ceea02ca 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 | |||
@@ -3,8 +3,16 @@ | |||
3 | 3 | ||
4 | <div ngbNav #nav="ngbNav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" class="nav-tabs"> | 4 | <div ngbNav #nav="ngbNav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" class="nav-tabs"> |
5 | 5 | ||
6 | <ng-container ngbNavItem="instance-homepage"> | ||
7 | <a ngbNavLink i18n>Homepage</a> | ||
8 | |||
9 | <ng-template ngbNavContent> | ||
10 | <my-edit-homepage [form]="form" [formErrors]="formErrors"></my-edit-homepage> | ||
11 | </ng-template> | ||
12 | </ng-container> | ||
13 | |||
6 | <ng-container ngbNavItem="instance-information"> | 14 | <ng-container ngbNavItem="instance-information"> |
7 | <a ngbNavLink i18n>Instance information</a> | 15 | <a ngbNavLink i18n>Information</a> |
8 | 16 | ||
9 | <ng-template ngbNavContent> | 17 | <ng-template ngbNavContent> |
10 | <my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems"> | 18 | <my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems"> |
@@ -13,7 +21,7 @@ | |||
13 | </ng-container> | 21 | </ng-container> |
14 | 22 | ||
15 | <ng-container ngbNavItem="basic-configuration"> | 23 | <ng-container ngbNavItem="basic-configuration"> |
16 | <a ngbNavLink i18n>Basic configuration</a> | 24 | <a ngbNavLink i18n>Basic</a> |
17 | 25 | ||
18 | <ng-template ngbNavContent> | 26 | <ng-template ngbNavContent> |
19 | <my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig"> | 27 | <my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig"> |
@@ -40,7 +48,7 @@ | |||
40 | </ng-container> | 48 | </ng-container> |
41 | 49 | ||
42 | <ng-container ngbNavItem="advanced-configuration"> | 50 | <ng-container ngbNavItem="advanced-configuration"> |
43 | <a ngbNavLink i18n>Advanced configuration</a> | 51 | <a ngbNavLink i18n>Advanced</a> |
44 | 52 | ||
45 | <ng-template ngbNavContent> | 53 | <ng-template ngbNavContent> |
46 | <my-edit-advanced-configuration [form]="form" [formErrors]="formErrors"> | 54 | <my-edit-advanced-configuration [form]="form" [formErrors]="formErrors"> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 4b35d65fc..dc8334dd0 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | 1 | ||
2 | import omit from 'lodash-es/omit' | ||
2 | import { forkJoin } from 'rxjs' | 3 | import { forkJoin } from 'rxjs' |
3 | import { SelectOptionsItem } from 'src/types/select-options-item.model' | 4 | import { SelectOptionsItem } from 'src/types/select-options-item.model' |
4 | import { Component, OnInit } from '@angular/core' | 5 | import { Component, OnInit } from '@angular/core' |
@@ -24,9 +25,14 @@ import { | |||
24 | } from '@app/shared/form-validators/custom-config-validators' | 25 | } from '@app/shared/form-validators/custom-config-validators' |
25 | import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' | 26 | import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' |
26 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 27 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
27 | import { CustomConfig, ServerConfig } from '@shared/models' | 28 | import { CustomPageService } from '@app/shared/shared-main/custom-page' |
29 | import { CustomConfig, CustomPage, ServerConfig } from '@shared/models' | ||
28 | import { EditConfigurationService } from './edit-configuration.service' | 30 | import { EditConfigurationService } from './edit-configuration.service' |
29 | 31 | ||
32 | type ComponentCustomConfig = CustomConfig & { | ||
33 | instanceCustomHomepage: CustomPage | ||
34 | } | ||
35 | |||
30 | @Component({ | 36 | @Component({ |
31 | selector: 'my-edit-custom-config', | 37 | selector: 'my-edit-custom-config', |
32 | templateUrl: './edit-custom-config.component.html', | 38 | templateUrl: './edit-custom-config.component.html', |
@@ -35,9 +41,11 @@ import { EditConfigurationService } from './edit-configuration.service' | |||
35 | export class EditCustomConfigComponent extends FormReactive implements OnInit { | 41 | export class EditCustomConfigComponent extends FormReactive implements OnInit { |
36 | activeNav: string | 42 | activeNav: string |
37 | 43 | ||
38 | customConfig: CustomConfig | 44 | customConfig: ComponentCustomConfig |
39 | serverConfig: ServerConfig | 45 | serverConfig: ServerConfig |
40 | 46 | ||
47 | homepage: CustomPage | ||
48 | |||
41 | languageItems: SelectOptionsItem[] = [] | 49 | languageItems: SelectOptionsItem[] = [] |
42 | categoryItems: SelectOptionsItem[] = [] | 50 | categoryItems: SelectOptionsItem[] = [] |
43 | 51 | ||
@@ -47,6 +55,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
47 | protected formValidatorService: FormValidatorService, | 55 | protected formValidatorService: FormValidatorService, |
48 | private notifier: Notifier, | 56 | private notifier: Notifier, |
49 | private configService: ConfigService, | 57 | private configService: ConfigService, |
58 | private customPage: CustomPageService, | ||
50 | private serverService: ServerService, | 59 | private serverService: ServerService, |
51 | private editConfigurationService: EditConfigurationService | 60 | private editConfigurationService: EditConfigurationService |
52 | ) { | 61 | ) { |
@@ -56,11 +65,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
56 | ngOnInit () { | 65 | ngOnInit () { |
57 | this.serverConfig = this.serverService.getTmpConfig() | 66 | this.serverConfig = this.serverService.getTmpConfig() |
58 | this.serverService.getConfig() | 67 | this.serverService.getConfig() |
59 | .subscribe(config => { | 68 | .subscribe(config => this.serverConfig = config) |
60 | this.serverConfig = config | ||
61 | }) | ||
62 | 69 | ||
63 | const formGroupData: { [key in keyof CustomConfig ]: any } = { | 70 | const formGroupData: { [key in keyof ComponentCustomConfig ]: any } = { |
64 | instance: { | 71 | instance: { |
65 | name: INSTANCE_NAME_VALIDATOR, | 72 | name: INSTANCE_NAME_VALIDATOR, |
66 | shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR, | 73 | shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR, |
@@ -215,6 +222,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
215 | disableLocalSearch: null, | 222 | disableLocalSearch: null, |
216 | isDefaultSearch: null | 223 | isDefaultSearch: null |
217 | } | 224 | } |
225 | }, | ||
226 | |||
227 | instanceCustomHomepage: { | ||
228 | content: null | ||
218 | } | 229 | } |
219 | } | 230 | } |
220 | 231 | ||
@@ -250,15 +261,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
250 | } | 261 | } |
251 | 262 | ||
252 | async formValidated () { | 263 | async formValidated () { |
253 | const value: CustomConfig = this.form.getRawValue() | 264 | const value: ComponentCustomConfig = this.form.getRawValue() |
254 | 265 | ||
255 | this.configService.updateCustomConfig(value) | 266 | forkJoin([ |
267 | this.configService.updateCustomConfig(omit(value, 'instanceCustomHomepage')), | ||
268 | this.customPage.updateInstanceHomepage(value.instanceCustomHomepage.content) | ||
269 | ]) | ||
256 | .subscribe( | 270 | .subscribe( |
257 | res => { | 271 | ([ resConfig ]) => { |
258 | this.customConfig = res | 272 | const instanceCustomHomepage = { |
273 | content: value.instanceCustomHomepage.content | ||
274 | } | ||
275 | |||
276 | this.customConfig = { ...resConfig, instanceCustomHomepage } | ||
259 | 277 | ||
260 | // Reload general configuration | 278 | // Reload general configuration |
261 | this.serverService.resetConfig() | 279 | this.serverService.resetConfig() |
280 | .subscribe(config => this.serverConfig = config) | ||
262 | 281 | ||
263 | this.updateForm() | 282 | this.updateForm() |
264 | 283 | ||
@@ -317,9 +336,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
317 | } | 336 | } |
318 | 337 | ||
319 | private loadConfigAndUpdateForm () { | 338 | private loadConfigAndUpdateForm () { |
320 | this.configService.getCustomConfig() | 339 | forkJoin([ |
321 | .subscribe(config => { | 340 | this.configService.getCustomConfig(), |
322 | this.customConfig = config | 341 | this.customPage.getInstanceHomepage() |
342 | ]) | ||
343 | .subscribe(([ config, homepage ]) => { | ||
344 | this.customConfig = { ...config, instanceCustomHomepage: homepage } | ||
323 | 345 | ||
324 | this.updateForm() | 346 | this.updateForm() |
325 | // Force form validation | 347 | // Force form validation |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html new file mode 100644 index 000000000..c48fa5bf8 --- /dev/null +++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html | |||
@@ -0,0 +1,28 @@ | |||
1 | <ng-container [formGroup]="form"> | ||
2 | |||
3 | <ng-container formGroupName="instanceCustomHomepage"> | ||
4 | |||
5 | <div class="form-row mt-5"> <!-- homepage grid --> | ||
6 | <div class="form-group col-12 col-lg-4 col-xl-3"> | ||
7 | <div i18n class="inner-form-title">INSTANCE HOMEPAGE</div> | ||
8 | </div> | ||
9 | |||
10 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> | ||
11 | |||
12 | <div class="form-group"> | ||
13 | <label i18n for="instanceCustomHomepageContent">Homepage</label> | ||
14 | |||
15 | <my-markdown-textarea | ||
16 | name="instanceCustomHomepageContent" formControlName="content" textareaMaxWidth="90%" textareaHeight="300px" | ||
17 | [customMarkdownRenderer]="customMarkdownRenderer" | ||
18 | [classes]="{ 'input-error': formErrors['instanceCustomHomepage.content'] }" | ||
19 | ></my-markdown-textarea> | ||
20 | |||
21 | <div *ngIf="formErrors.instanceCustomHomepage.content" class="form-error">{{ formErrors.instanceCustomHomepage.content }}</div> | ||
22 | </div> | ||
23 | </div> | ||
24 | </div> | ||
25 | |||
26 | </ng-container> | ||
27 | |||
28 | </ng-container> | ||
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts new file mode 100644 index 000000000..7decf8f75 --- /dev/null +++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { FormGroup } from '@angular/forms' | ||
3 | import { CustomMarkupService } from '@app/shared/shared-custom-markup' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-edit-homepage', | ||
7 | templateUrl: './edit-homepage.component.html', | ||
8 | styleUrls: [ './edit-custom-config.component.scss' ] | ||
9 | }) | ||
10 | export class EditHomepageComponent implements OnInit { | ||
11 | @Input() form: FormGroup | ||
12 | @Input() formErrors: any | ||
13 | |||
14 | customMarkdownRenderer: (text: string) => Promise<HTMLElement> | ||
15 | |||
16 | constructor (private customMarkup: CustomMarkupService) { | ||
17 | |||
18 | } | ||
19 | |||
20 | ngOnInit () { | ||
21 | this.customMarkdownRenderer = async (text: string) => { | ||
22 | return this.customMarkup.buildElement(text) | ||
23 | } | ||
24 | } | ||
25 | } | ||
diff --git a/client/src/app/+admin/config/edit-custom-config/index.ts b/client/src/app/+admin/config/edit-custom-config/index.ts index 95fcc8f52..4281ad09b 100644 --- a/client/src/app/+admin/config/edit-custom-config/index.ts +++ b/client/src/app/+admin/config/edit-custom-config/index.ts | |||
@@ -2,6 +2,7 @@ export * from './edit-advanced-configuration.component' | |||
2 | export * from './edit-basic-configuration.component' | 2 | export * from './edit-basic-configuration.component' |
3 | export * from './edit-configuration.service' | 3 | export * from './edit-configuration.service' |
4 | export * from './edit-custom-config.component' | 4 | export * from './edit-custom-config.component' |
5 | export * from './edit-homepage.component' | ||
5 | export * from './edit-instance-information.component' | 6 | export * from './edit-instance-information.component' |
6 | export * from './edit-live-configuration.component' | 7 | export * from './edit-live-configuration.component' |
7 | export * from './edit-vod-transcoding.component' | 8 | export * from './edit-vod-transcoding.component' |
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts index 1a95980ae..6af224920 100644 --- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts +++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts | |||
@@ -5,8 +5,7 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' | |||
5 | import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core' | 5 | import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core' |
6 | import { PluginService } from '@app/core/plugins/plugin.service' | 6 | import { PluginService } from '@app/core/plugins/plugin.service' |
7 | import { compareSemVer } from '@shared/core-utils/miscs/miscs' | 7 | import { compareSemVer } from '@shared/core-utils/miscs/miscs' |
8 | import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' | 8 | import { PeerTubePlugin, PluginType } from '@shared/models' |
9 | import { PluginType } from '@shared/models/plugins/plugin.type' | ||
10 | 9 | ||
11 | @Component({ | 10 | @Component({ |
12 | selector: 'my-plugin-list-installed', | 11 | selector: 'my-plugin-list-installed', |
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 6900e8717..8d8f12c48 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 | |||
@@ -20,7 +20,7 @@ | |||
20 | <my-global-icon iconName="search"></my-global-icon> | 20 | <my-global-icon iconName="search"></my-global-icon> |
21 | 21 | ||
22 | <ng-container i18n> | 22 | <ng-container i18n> |
23 | {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}" | 23 | {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}" |
24 | </ng-container> | 24 | </ng-container> |
25 | </ng-container> | 25 | </ng-container> |
26 | </div> | 26 | </div> |
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 d2c179aba..0a6e57904 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 | |||
@@ -4,8 +4,7 @@ import { Component, OnInit } from '@angular/core' | |||
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' | 5 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' |
6 | import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core' | 6 | import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core' |
7 | import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' | 7 | import { PeerTubePluginIndex, PluginType } from '@shared/models' |
8 | import { PluginType } from '@shared/models/plugins/plugin.type' | ||
9 | 8 | ||
10 | @Component({ | 9 | @Component({ |
11 | selector: 'my-plugin-search', | 10 | selector: 'my-plugin-search', |
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 281c3dcef..1527508f7 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 | |||
@@ -81,6 +81,8 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
81 | userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10) | 81 | userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10) |
82 | userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) | 82 | userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) |
83 | 83 | ||
84 | if (userUpdate.pluginAuth === 'null') userUpdate.pluginAuth = null | ||
85 | |||
84 | this.userService.updateUser(this.user.id, userUpdate).subscribe( | 86 | this.userService.updateUser(this.user.id, userUpdate).subscribe( |
85 | () => { | 87 | () => { |
86 | this.notifier.success($localize`User ${this.user.username} updated.`) | 88 | this.notifier.success($localize`User ${this.user.username} updated.`) |
diff --git a/client/src/app/+home/home-routing.module.ts b/client/src/app/+home/home-routing.module.ts new file mode 100644 index 000000000..1eaee4449 --- /dev/null +++ b/client/src/app/+home/home-routing.module.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RouterModule, Routes } from '@angular/router' | ||
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { HomeComponent } from './home.component' | ||
5 | |||
6 | const homeRoutes: Routes = [ | ||
7 | { | ||
8 | path: '', | ||
9 | component: HomeComponent, | ||
10 | canActivateChild: [ MetaGuard ] | ||
11 | } | ||
12 | ] | ||
13 | |||
14 | @NgModule({ | ||
15 | imports: [ RouterModule.forChild(homeRoutes) ], | ||
16 | exports: [ RouterModule ] | ||
17 | }) | ||
18 | export class HomeRoutingModule {} | ||
diff --git a/client/src/app/+home/home.component.html b/client/src/app/+home/home.component.html new file mode 100644 index 000000000..645b9dc69 --- /dev/null +++ b/client/src/app/+home/home.component.html | |||
@@ -0,0 +1,4 @@ | |||
1 | <div class="root margin-content"> | ||
2 | <div #contentWrapper></div> | ||
3 | </div> | ||
4 | |||
diff --git a/client/src/app/+home/home.component.scss b/client/src/app/+home/home.component.scss new file mode 100644 index 000000000..6c73e9248 --- /dev/null +++ b/client/src/app/+home/home.component.scss | |||
@@ -0,0 +1,3 @@ | |||
1 | .root { | ||
2 | padding-top: 20px; | ||
3 | } | ||
diff --git a/client/src/app/+home/home.component.ts b/client/src/app/+home/home.component.ts new file mode 100644 index 000000000..16d3a6df7 --- /dev/null +++ b/client/src/app/+home/home.component.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | |||
2 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' | ||
3 | import { CustomMarkupService } from '@app/shared/shared-custom-markup' | ||
4 | import { CustomPageService } from '@app/shared/shared-main/custom-page' | ||
5 | |||
6 | @Component({ | ||
7 | templateUrl: './home.component.html', | ||
8 | styleUrls: [ './home.component.scss' ] | ||
9 | }) | ||
10 | |||
11 | export class HomeComponent implements OnInit { | ||
12 | @ViewChild('contentWrapper') contentWrapper: ElementRef<HTMLInputElement> | ||
13 | |||
14 | constructor ( | ||
15 | private customMarkupService: CustomMarkupService, | ||
16 | private customPageService: CustomPageService | ||
17 | ) { } | ||
18 | |||
19 | async ngOnInit () { | ||
20 | this.customPageService.getInstanceHomepage() | ||
21 | .subscribe(async ({ content }) => { | ||
22 | const element = await this.customMarkupService.buildElement(content) | ||
23 | this.contentWrapper.nativeElement.appendChild(element) | ||
24 | }) | ||
25 | } | ||
26 | } | ||
diff --git a/client/src/app/+home/home.module.ts b/client/src/app/+home/home.module.ts new file mode 100644 index 000000000..102cdc296 --- /dev/null +++ b/client/src/app/+home/home.module.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup' | ||
3 | import { SharedMainModule } from '@app/shared/shared-main' | ||
4 | import { HomeRoutingModule } from './home-routing.module' | ||
5 | import { HomeComponent } from './home.component' | ||
6 | |||
7 | @NgModule({ | ||
8 | imports: [ | ||
9 | HomeRoutingModule, | ||
10 | |||
11 | SharedMainModule, | ||
12 | SharedCustomMarkupModule | ||
13 | ], | ||
14 | |||
15 | declarations: [ | ||
16 | HomeComponent | ||
17 | ], | ||
18 | |||
19 | exports: [ | ||
20 | HomeComponent | ||
21 | ], | ||
22 | |||
23 | providers: [ ] | ||
24 | }) | ||
25 | export class HomeModule { } | ||
diff --git a/client/src/app/+home/index.ts b/client/src/app/+home/index.ts new file mode 100644 index 000000000..7c77cf9fd --- /dev/null +++ b/client/src/app/+home/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './home-routing.module' | ||
2 | export * from './home.component' | ||
3 | export * from './home.module' | ||
diff --git a/client/src/app/+login/login-routing.module.ts b/client/src/app/+login/login-routing.module.ts index 258ddc5c1..c5f0f23c2 100644 --- a/client/src/app/+login/login-routing.module.ts +++ b/client/src/app/+login/login-routing.module.ts | |||
@@ -1,14 +1,12 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { LoginComponent } from './login.component' | ||
5 | import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' | 3 | import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' |
4 | import { LoginComponent } from './login.component' | ||
6 | 5 | ||
7 | const loginRoutes: Routes = [ | 6 | const loginRoutes: Routes = [ |
8 | { | 7 | { |
9 | path: '', | 8 | path: '', |
10 | component: LoginComponent, | 9 | component: LoginComponent, |
11 | canActivate: [ MetaGuard ], | ||
12 | data: { | 10 | data: { |
13 | meta: { | 11 | meta: { |
14 | title: $localize`Login` | 12 | title: $localize`Login` |
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index e2f8660fb..ef39c1a36 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts | |||
@@ -1,20 +1,19 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { LoginGuard } from '../core' | 3 | import { LoginGuard } from '../core' |
5 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' | 4 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' |
5 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' | ||
6 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' | 6 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' |
7 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' | 7 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' |
8 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' | 8 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' |
9 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 9 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
10 | import { MyAccountComponent } from './my-account.component' | 10 | import { MyAccountComponent } from './my-account.component' |
11 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' | ||
12 | 11 | ||
13 | const myAccountRoutes: Routes = [ | 12 | const myAccountRoutes: Routes = [ |
14 | { | 13 | { |
15 | path: '', | 14 | path: '', |
16 | component: MyAccountComponent, | 15 | component: MyAccountComponent, |
17 | canActivateChild: [ MetaGuard, LoginGuard ], | 16 | canActivateChild: [ LoginGuard ], |
18 | children: [ | 17 | children: [ |
19 | { | 18 | { |
20 | path: '', | 19 | path: '', |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index c16368952..a0f2f28f8 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts | |||
@@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common' | |||
2 | import { HttpErrorResponse } from '@angular/common/http' | 2 | import { HttpErrorResponse } from '@angular/common/http' |
3 | import { AfterViewChecked, Component, OnInit } from '@angular/core' | 3 | import { AfterViewChecked, Component, OnInit } from '@angular/core' |
4 | import { AuthService, Notifier, User, UserService } from '@app/core' | 4 | import { AuthService, Notifier, User, UserService } from '@app/core' |
5 | import { uploadErrorHandler } from '@app/helpers' | 5 | import { genericUploadErrorHandler } from '@app/helpers' |
6 | 6 | ||
7 | @Component({ | 7 | @Component({ |
8 | selector: 'my-account-settings', | 8 | selector: 'my-account-settings', |
@@ -46,7 +46,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { | |||
46 | this.user.updateAccountAvatar(data.avatar) | 46 | this.user.updateAccountAvatar(data.avatar) |
47 | }, | 47 | }, |
48 | 48 | ||
49 | (err: HttpErrorResponse) => uploadErrorHandler({ | 49 | (err: HttpErrorResponse) => genericUploadErrorHandler({ |
50 | err, | 50 | err, |
51 | name: $localize`avatar`, | 51 | name: $localize`avatar`, |
52 | notifier: this.notifier | 52 | notifier: this.notifier |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts index a29af176c..c9173039a 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts | |||
@@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http' | |||
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, Notifier, ServerService } from '@app/core' |
6 | import { uploadErrorHandler } from '@app/helpers' | 6 | import { genericUploadErrorHandler } from '@app/helpers' |
7 | import { | 7 | import { |
8 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, | 8 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, |
9 | VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, | 9 | VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, |
@@ -109,7 +109,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
109 | this.videoChannel.updateAvatar(data.avatar) | 109 | this.videoChannel.updateAvatar(data.avatar) |
110 | }, | 110 | }, |
111 | 111 | ||
112 | (err: HttpErrorResponse) => uploadErrorHandler({ | 112 | (err: HttpErrorResponse) => genericUploadErrorHandler({ |
113 | err, | 113 | err, |
114 | name: $localize`avatar`, | 114 | name: $localize`avatar`, |
115 | notifier: this.notifier | 115 | notifier: this.notifier |
@@ -139,7 +139,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
139 | this.videoChannel.updateBanner(data.banner) | 139 | this.videoChannel.updateBanner(data.banner) |
140 | }, | 140 | }, |
141 | 141 | ||
142 | (err: HttpErrorResponse) => uploadErrorHandler({ | 142 | (err: HttpErrorResponse) => genericUploadErrorHandler({ |
143 | err, | 143 | err, |
144 | name: $localize`banner`, | 144 | name: $localize`banner`, |
145 | notifier: this.notifier | 145 | notifier: this.notifier |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts index 9e3bf35b4..67b3ee496 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts | |||
@@ -68,8 +68,14 @@ channel with the same name (${videoChannel.name})!`, | |||
68 | this.authService.userInformationLoaded | 68 | this.authService.userInformationLoaded |
69 | .pipe(mergeMap(() => { | 69 | .pipe(mergeMap(() => { |
70 | const user = this.authService.getUser() | 70 | const user = this.authService.getUser() |
71 | const options = { | ||
72 | account: user.account, | ||
73 | withStats: true, | ||
74 | search: this.search, | ||
75 | sort: '-updatedAt' | ||
76 | } | ||
71 | 77 | ||
72 | return this.videoChannelService.listAccountVideoChannels(user.account, null, true, this.search) | 78 | return this.videoChannelService.listAccountVideoChannels(options) |
73 | })).subscribe(res => { | 79 | })).subscribe(res => { |
74 | this.videoChannels = res.data | 80 | this.videoChannels = res.data |
75 | this.totalItems = res.total | 81 | this.totalItems = res.total |
diff --git a/client/src/app/+my-library/my-library-routing.module.ts b/client/src/app/+my-library/my-library-routing.module.ts index d8e5aa562..76894bed8 100644 --- a/client/src/app/+my-library/my-library-routing.module.ts +++ b/client/src/app/+my-library/my-library-routing.module.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { LoginGuard } from '../core' | 3 | import { LoginGuard } from '../core' |
5 | import { MyHistoryComponent } from './my-history/my-history.component' | 4 | import { MyHistoryComponent } from './my-history/my-history.component' |
6 | import { MyLibraryComponent } from './my-library.component' | 5 | import { MyLibraryComponent } from './my-library.component' |
@@ -17,7 +16,7 @@ const myLibraryRoutes: Routes = [ | |||
17 | { | 16 | { |
18 | path: '', | 17 | path: '', |
19 | component: MyLibraryComponent, | 18 | component: MyLibraryComponent, |
20 | canActivateChild: [ MetaGuard, LoginGuard ], | 19 | canActivateChild: [ LoginGuard ], |
21 | children: [ | 20 | children: [ |
22 | { | 21 | { |
23 | path: '', | 22 | path: '', |
diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html index 088765b20..d0393a2a4 100644 --- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html | |||
@@ -8,13 +8,8 @@ | |||
8 | <div class="modal-body" [formGroup]="form"> | 8 | <div class="modal-body" [formGroup]="form"> |
9 | <div class="form-group"> | 9 | <div class="form-group"> |
10 | <label i18n for="channel">Select a channel to receive the video</label> | 10 | <label i18n for="channel">Select a channel to receive the video</label> |
11 | <div class="peertube-select-container"> | 11 | <my-select-channel labelForId="channel" formControlName="channel" [items]="videoChannels"></my-select-channel> |
12 | <select formControlName="channel" id="channel" class="form-control"> | 12 | |
13 | <option i18n value="undefined" disabled>Channel that will receive the video</option> | ||
14 | <option *ngFor="let channel of videoChannels" [value]="channel.id">{{ channel.displayName }} | ||
15 | </option> | ||
16 | </select> | ||
17 | </div> | ||
18 | <div *ngIf="formErrors.channel" class="form-error">{{ formErrors.channel }}</div> | 13 | <div *ngIf="formErrors.channel" class="form-error">{{ formErrors.channel }}</div> |
19 | </div> | 14 | </div> |
20 | </div> | 15 | </div> |
diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts index 0e2395754..7889d0985 100644 --- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { switchMap } from 'rxjs/operators' | 1 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
2 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { listUserChannels } from '@app/helpers' | ||
4 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' | 5 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' |
5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
6 | import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' | 7 | import { VideoOwnershipService } from '@app/shared/shared-main' |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 8 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { VideoChangeOwnership, VideoChannel } from '@shared/models' | 9 | import { VideoChangeOwnership } from '@shared/models' |
9 | 10 | ||
10 | @Component({ | 11 | @Component({ |
11 | selector: 'my-accept-ownership', | 12 | selector: 'my-accept-ownership', |
@@ -18,8 +19,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { | |||
18 | @ViewChild('modal', { static: true }) modal: ElementRef | 19 | @ViewChild('modal', { static: true }) modal: ElementRef |
19 | 20 | ||
20 | videoChangeOwnership: VideoChangeOwnership | undefined = undefined | 21 | videoChangeOwnership: VideoChangeOwnership | undefined = undefined |
21 | 22 | videoChannels: SelectChannelItem[] | |
22 | videoChannels: VideoChannel[] | ||
23 | 23 | ||
24 | error: string = null | 24 | error: string = null |
25 | 25 | ||
@@ -28,7 +28,6 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { | |||
28 | private videoOwnershipService: VideoOwnershipService, | 28 | private videoOwnershipService: VideoOwnershipService, |
29 | private notifier: Notifier, | 29 | private notifier: Notifier, |
30 | private authService: AuthService, | 30 | private authService: AuthService, |
31 | private videoChannelService: VideoChannelService, | ||
32 | private modalService: NgbModal | 31 | private modalService: NgbModal |
33 | ) { | 32 | ) { |
34 | super() | 33 | super() |
@@ -37,9 +36,8 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { | |||
37 | ngOnInit () { | 36 | ngOnInit () { |
38 | this.videoChannels = [] | 37 | this.videoChannels = [] |
39 | 38 | ||
40 | this.authService.userInformationLoaded | 39 | listUserChannels(this.authService) |
41 | .pipe(switchMap(() => this.videoChannelService.listAccountVideoChannels(this.authService.getUser().account))) | 40 | .subscribe(channels => this.videoChannels = channels) |
42 | .subscribe(videoChannels => this.videoChannels = videoChannels.data) | ||
43 | 41 | ||
44 | this.buildForm({ | 42 | this.buildForm({ |
45 | channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR | 43 | channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR |
diff --git a/client/src/app/+reset-password/reset-password-routing.module.ts b/client/src/app/+reset-password/reset-password-routing.module.ts index 7f1ba2f68..3532cdbc1 100644 --- a/client/src/app/+reset-password/reset-password-routing.module.ts +++ b/client/src/app/+reset-password/reset-password-routing.module.ts | |||
@@ -1,16 +1,14 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { ResetPasswordComponent } from './reset-password.component' | 3 | import { ResetPasswordComponent } from './reset-password.component' |
5 | 4 | ||
6 | const resetPasswordRoutes: Routes = [ | 5 | const resetPasswordRoutes: Routes = [ |
7 | { | 6 | { |
8 | path: '', | 7 | path: '', |
9 | component: ResetPasswordComponent, | 8 | component: ResetPasswordComponent, |
10 | canActivate: [ MetaGuard ], | ||
11 | data: { | 9 | data: { |
12 | meta: { | 10 | meta: { |
13 | title: `Reset password` | 11 | title: $localize`Reset password` |
14 | } | 12 | } |
15 | } | 13 | } |
16 | } | 14 | } |
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html index 1d1e7b868..421bc7f6f 100644 --- a/client/src/app/+search/search-filters.component.html +++ b/client/src/app/+search/search-filters.component.html | |||
@@ -18,6 +18,25 @@ | |||
18 | 18 | ||
19 | <div class="form-group"> | 19 | <div class="form-group"> |
20 | <div class="radio-label label-container"> | 20 | <div class="radio-label label-container"> |
21 | <label i18n>Display only</label> | ||
22 | <button i18n class="reset-button reset-button-small" (click)="resetField('isLive')" *ngIf="advancedSearch.isLive !== undefined"> | ||
23 | Reset | ||
24 | </button> | ||
25 | </div> | ||
26 | |||
27 | <div class="peertube-radio-container"> | ||
28 | <input type="radio" name="isLive" id="isLiveTrue" value="true" [(ngModel)]="advancedSearch.isLive"> | ||
29 | <label i18n for="isLiveTrue" class="radio">Live videos</label> | ||
30 | </div> | ||
31 | |||
32 | <div class="peertube-radio-container"> | ||
33 | <input type="radio" name="isLive" id="isLiveFalse" value="false" [(ngModel)]="advancedSearch.isLive"> | ||
34 | <label i18n for="isLiveFalse" class="radio">VOD videos</label> | ||
35 | </div> | ||
36 | </div> | ||
37 | |||
38 | <div class="form-group"> | ||
39 | <div class="radio-label label-container"> | ||
21 | <label i18n>Display sensitive content</label> | 40 | <label i18n>Display sensitive content</label> |
22 | <button i18n class="reset-button reset-button-small" (click)="resetField('nsfw')" *ngIf="advancedSearch.nsfw !== undefined"> | 41 | <button i18n class="reset-button reset-button-small" (click)="resetField('nsfw')" *ngIf="advancedSearch.nsfw !== undefined"> |
23 | Reset | 42 | Reset |
@@ -44,7 +63,7 @@ | |||
44 | </div> | 63 | </div> |
45 | 64 | ||
46 | <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges"> | 65 | <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges"> |
47 | <input type="radio" (change)="inputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange"> | 66 | <input type="radio" (change)="onInputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange"> |
48 | <label [for]="date.id" class="radio">{{ date.label }}</label> | 67 | <label [for]="date.id" class="radio">{{ date.label }}</label> |
49 | </div> | 68 | </div> |
50 | </div> | 69 | </div> |
@@ -60,7 +79,7 @@ | |||
60 | <div class="row"> | 79 | <div class="row"> |
61 | <div class="pl-0 col-sm-6"> | 80 | <div class="pl-0 col-sm-6"> |
62 | <input | 81 | <input |
63 | (change)="inputUpdated()" | 82 | (change)="onInputUpdated()" |
64 | (keydown.enter)="$event.preventDefault()" | 83 | (keydown.enter)="$event.preventDefault()" |
65 | type="text" id="original-publication-after" name="original-publication-after" | 84 | type="text" id="original-publication-after" name="original-publication-after" |
66 | i18n-placeholder placeholder="After..." | 85 | i18n-placeholder placeholder="After..." |
@@ -70,7 +89,7 @@ | |||
70 | </div> | 89 | </div> |
71 | <div class="pr-0 col-sm-6"> | 90 | <div class="pr-0 col-sm-6"> |
72 | <input | 91 | <input |
73 | (change)="inputUpdated()" | 92 | (change)="onInputUpdated()" |
74 | (keydown.enter)="$event.preventDefault()" | 93 | (keydown.enter)="$event.preventDefault()" |
75 | type="text" id="original-publication-before" name="original-publication-before" | 94 | type="text" id="original-publication-before" name="original-publication-before" |
76 | i18n-placeholder placeholder="Before..." | 95 | i18n-placeholder placeholder="Before..." |
@@ -93,7 +112,7 @@ | |||
93 | </div> | 112 | </div> |
94 | 113 | ||
95 | <div class="peertube-radio-container" *ngFor="let duration of durationRanges"> | 114 | <div class="peertube-radio-container" *ngFor="let duration of durationRanges"> |
96 | <input type="radio" (change)="inputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange"> | 115 | <input type="radio" (change)="onInputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange"> |
97 | <label [for]="duration.id" class="radio">{{ duration.label }}</label> | 116 | <label [for]="duration.id" class="radio">{{ duration.label }}</label> |
98 | </div> | 117 | </div> |
99 | </div> | 118 | </div> |
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts index a2af9a942..59aba22ff 100644 --- a/client/src/app/+search/search-filters.component.ts +++ b/client/src/app/+search/search-filters.component.ts | |||
@@ -3,6 +3,8 @@ import { ServerService } from '@app/core' | |||
3 | import { AdvancedSearch } from '@app/shared/shared-search' | 3 | import { AdvancedSearch } from '@app/shared/shared-search' |
4 | import { ServerConfig, VideoConstant } from '@shared/models' | 4 | import { ServerConfig, VideoConstant } from '@shared/models' |
5 | 5 | ||
6 | type FormOption = { id: string, label: string } | ||
7 | |||
6 | @Component({ | 8 | @Component({ |
7 | selector: 'my-search-filters', | 9 | selector: 'my-search-filters', |
8 | styleUrls: [ './search-filters.component.scss' ], | 10 | styleUrls: [ './search-filters.component.scss' ], |
@@ -17,9 +19,10 @@ export class SearchFiltersComponent implements OnInit { | |||
17 | videoLicences: VideoConstant<number>[] = [] | 19 | videoLicences: VideoConstant<number>[] = [] |
18 | videoLanguages: VideoConstant<string>[] = [] | 20 | videoLanguages: VideoConstant<string>[] = [] |
19 | 21 | ||
20 | publishedDateRanges: { id: string, label: string }[] = [] | 22 | publishedDateRanges: FormOption[] = [] |
21 | sorts: { id: string, label: string }[] = [] | 23 | sorts: FormOption[] = [] |
22 | durationRanges: { id: string, label: string }[] = [] | 24 | durationRanges: FormOption[] = [] |
25 | videoType: FormOption[] = [] | ||
23 | 26 | ||
24 | publishedDateRange: string | 27 | publishedDateRange: string |
25 | durationRange: string | 28 | durationRange: string |
@@ -34,10 +37,6 @@ export class SearchFiltersComponent implements OnInit { | |||
34 | ) { | 37 | ) { |
35 | this.publishedDateRanges = [ | 38 | this.publishedDateRanges = [ |
36 | { | 39 | { |
37 | id: 'any_published_date', | ||
38 | label: $localize`Any` | ||
39 | }, | ||
40 | { | ||
41 | id: 'today', | 40 | id: 'today', |
42 | label: $localize`Today` | 41 | label: $localize`Today` |
43 | }, | 42 | }, |
@@ -55,12 +54,19 @@ export class SearchFiltersComponent implements OnInit { | |||
55 | } | 54 | } |
56 | ] | 55 | ] |
57 | 56 | ||
58 | this.durationRanges = [ | 57 | this.videoType = [ |
59 | { | 58 | { |
60 | id: 'any_duration', | 59 | id: 'vod', |
61 | label: $localize`Any` | 60 | label: $localize`VOD videos` |
62 | }, | 61 | }, |
63 | { | 62 | { |
63 | id: 'live', | ||
64 | label: $localize`Live videos` | ||
65 | } | ||
66 | ] | ||
67 | |||
68 | this.durationRanges = [ | ||
69 | { | ||
64 | id: 'short', | 70 | id: 'short', |
65 | label: $localize`Short (< 4 min)` | 71 | label: $localize`Short (< 4 min)` |
66 | }, | 72 | }, |
@@ -104,24 +110,26 @@ export class SearchFiltersComponent implements OnInit { | |||
104 | this.loadOriginallyPublishedAtYears() | 110 | this.loadOriginallyPublishedAtYears() |
105 | } | 111 | } |
106 | 112 | ||
107 | inputUpdated () { | 113 | onInputUpdated () { |
108 | this.updateModelFromDurationRange() | 114 | this.updateModelFromDurationRange() |
109 | this.updateModelFromPublishedRange() | 115 | this.updateModelFromPublishedRange() |
110 | this.updateModelFromOriginallyPublishedAtYears() | 116 | this.updateModelFromOriginallyPublishedAtYears() |
111 | } | 117 | } |
112 | 118 | ||
113 | formUpdated () { | 119 | formUpdated () { |
114 | this.inputUpdated() | 120 | this.onInputUpdated() |
115 | this.filtered.emit(this.advancedSearch) | 121 | this.filtered.emit(this.advancedSearch) |
116 | } | 122 | } |
117 | 123 | ||
118 | reset () { | 124 | reset () { |
119 | this.advancedSearch.reset() | 125 | this.advancedSearch.reset() |
126 | |||
127 | this.resetOriginalPublicationYears() | ||
128 | |||
120 | this.durationRange = undefined | 129 | this.durationRange = undefined |
121 | this.publishedDateRange = undefined | 130 | this.publishedDateRange = undefined |
122 | this.originallyPublishedStartYear = undefined | 131 | |
123 | this.originallyPublishedEndYear = undefined | 132 | this.onInputUpdated() |
124 | this.inputUpdated() | ||
125 | } | 133 | } |
126 | 134 | ||
127 | resetField (fieldName: string, value?: any) { | 135 | resetField (fieldName: string, value?: any) { |
@@ -130,7 +138,7 @@ export class SearchFiltersComponent implements OnInit { | |||
130 | 138 | ||
131 | resetLocalField (fieldName: string, value?: any) { | 139 | resetLocalField (fieldName: string, value?: any) { |
132 | this[fieldName] = value | 140 | this[fieldName] = value |
133 | this.inputUpdated() | 141 | this.onInputUpdated() |
134 | } | 142 | } |
135 | 143 | ||
136 | resetOriginalPublicationYears () { | 144 | resetOriginalPublicationYears () { |
diff --git a/client/src/app/+search/search-routing.module.ts b/client/src/app/+search/search-routing.module.ts index e5d7d1ede..0d778af0d 100644 --- a/client/src/app/+search/search-routing.module.ts +++ b/client/src/app/+search/search-routing.module.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' | 3 | import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' |
5 | import { SearchComponent } from './search.component' | 4 | import { SearchComponent } from './search.component' |
6 | import { VideoLazyLoadResolver } from './video-lazy-load.resolver' | 5 | import { VideoLazyLoadResolver } from './video-lazy-load.resolver' |
@@ -9,7 +8,6 @@ const searchRoutes: Routes = [ | |||
9 | { | 8 | { |
10 | path: '', | 9 | path: '', |
11 | component: SearchComponent, | 10 | component: SearchComponent, |
12 | canActivate: [ MetaGuard ], | ||
13 | data: { | 11 | data: { |
14 | meta: { | 12 | meta: { |
15 | title: $localize`Search` | 13 | title: $localize`Search` |
@@ -19,7 +17,6 @@ const searchRoutes: Routes = [ | |||
19 | { | 17 | { |
20 | path: 'lazy-load-video', | 18 | path: 'lazy-load-video', |
21 | component: SearchComponent, | 19 | component: SearchComponent, |
22 | canActivate: [ MetaGuard ], | ||
23 | resolve: { | 20 | resolve: { |
24 | data: VideoLazyLoadResolver | 21 | data: VideoLazyLoadResolver |
25 | } | 22 | } |
@@ -27,7 +24,6 @@ const searchRoutes: Routes = [ | |||
27 | { | 24 | { |
28 | path: 'lazy-load-channel', | 25 | path: 'lazy-load-channel', |
29 | component: SearchComponent, | 26 | component: SearchComponent, |
30 | canActivate: [ MetaGuard ], | ||
31 | resolve: { | 27 | resolve: { |
32 | data: ChannelLazyLoadResolver | 28 | data: ChannelLazyLoadResolver |
33 | } | 29 | } |
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts index 8a781cbe4..4381659e1 100644 --- a/client/src/app/+search/search.component.ts +++ b/client/src/app/+search/search.component.ts | |||
@@ -1,12 +1,11 @@ | |||
1 | import { forkJoin, of, Subscription } from 'rxjs' | 1 | import { forkJoin, of, Subscription } from 'rxjs' |
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | 2 | import { Component, OnDestroy, OnInit } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, ComponentPagination, HooksService, Notifier, ServerService, User, UserService } from '@app/core' | 4 | import { AuthService, ComponentPagination, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core' |
5 | import { immutableAssign } from '@app/helpers' | 5 | import { immutableAssign } from '@app/helpers' |
6 | import { Video, VideoChannel } from '@app/shared/shared-main' | 6 | import { Video, VideoChannel } from '@app/shared/shared-main' |
7 | import { AdvancedSearch, SearchService } from '@app/shared/shared-search' | 7 | import { AdvancedSearch, SearchService } from '@app/shared/shared-search' |
8 | import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature' | 8 | import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature' |
9 | import { MetaService } from '@ngx-meta/core' | ||
10 | import { SearchTargetType, ServerConfig } from '@shared/models' | 9 | import { SearchTargetType, ServerConfig } from '@shared/models' |
11 | 10 | ||
12 | @Component({ | 11 | @Component({ |
@@ -238,7 +237,10 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
238 | } | 237 | } |
239 | 238 | ||
240 | private updateTitle () { | 239 | private updateTitle () { |
241 | const suffix = this.currentSearch ? ' ' + this.currentSearch : '' | 240 | const suffix = this.currentSearch |
241 | ? ' ' + this.currentSearch | ||
242 | : '' | ||
243 | |||
242 | this.metaService.setTitle($localize`Search` + suffix) | 244 | this.metaService.setTitle($localize`Search` + suffix) |
243 | } | 245 | } |
244 | 246 | ||
diff --git a/client/src/app/+signup/+register/register-routing.module.ts b/client/src/app/+signup/+register/register-routing.module.ts index 61a2fa42d..dabe79fa5 100644 --- a/client/src/app/+signup/+register/register-routing.module.ts +++ b/client/src/app/+signup/+register/register-routing.module.ts | |||
@@ -1,14 +1,13 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { ServerConfigResolver, UnloggedGuard } from '@app/core' | 3 | import { ServerConfigResolver, UnloggedGuard } from '@app/core' |
4 | import { MetaGuard } from '@ngx-meta/core' | ||
5 | import { RegisterComponent } from './register.component' | 4 | import { RegisterComponent } from './register.component' |
6 | 5 | ||
7 | const registerRoutes: Routes = [ | 6 | const registerRoutes: Routes = [ |
8 | { | 7 | { |
9 | path: '', | 8 | path: '', |
10 | component: RegisterComponent, | 9 | component: RegisterComponent, |
11 | canActivate: [ MetaGuard, UnloggedGuard ], | 10 | canActivate: [ UnloggedGuard ], |
12 | data: { | 11 | data: { |
13 | meta: { | 12 | meta: { |
14 | title: $localize`Register` | 13 | title: $localize`Register` |
diff --git a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts index 67c80ae93..1bc636345 100644 --- a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts +++ b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts | |||
@@ -1,13 +1,11 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component' | ||
5 | import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' | 3 | import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' |
4 | import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component' | ||
6 | 5 | ||
7 | const verifyAccountRoutes: Routes = [ | 6 | const verifyAccountRoutes: Routes = [ |
8 | { | 7 | { |
9 | path: '', | 8 | path: '', |
10 | canActivateChild: [ MetaGuard ], | ||
11 | children: [ | 9 | children: [ |
12 | { | 10 | { |
13 | path: 'email', | 11 | path: 'email', |
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts index fcaad8934..4ee052873 100644 --- a/client/src/app/+video-channels/video-channels-routing.module.ts +++ b/client/src/app/+video-channels/video-channels-routing.module.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' | 3 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' |
5 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' | 4 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' |
6 | import { VideoChannelsComponent } from './video-channels.component' | 5 | import { VideoChannelsComponent } from './video-channels.component' |
@@ -9,7 +8,6 @@ const videoChannelsRoutes: Routes = [ | |||
9 | { | 8 | { |
10 | path: ':videoChannelName', | 9 | path: ':videoChannelName', |
11 | component: VideoChannelsComponent, | 10 | component: VideoChannelsComponent, |
12 | canActivateChild: [ MetaGuard ], | ||
13 | children: [ | 11 | children: [ |
14 | { | 12 | { |
15 | path: '', | 13 | path: '', |
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 094b4d3b3..50d030ac9 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 | |||
@@ -5,7 +5,7 @@ | |||
5 | <a ngbNavLink i18n>Basic info</a> | 5 | <a ngbNavLink i18n>Basic info</a> |
6 | 6 | ||
7 | <ng-template ngbNavContent> | 7 | <ng-template ngbNavContent> |
8 | <div class="row"> | 8 | <div class="form-columns"> |
9 | <div class="col-video-edit"> | 9 | <div class="col-video-edit"> |
10 | <div class="form-group"> | 10 | <div class="form-group"> |
11 | <label i18n for="name">Title</label> | 11 | <label i18n for="name">Title</label> |
@@ -76,7 +76,7 @@ | |||
76 | <my-help> | 76 | <my-help> |
77 | <ng-template ptTemplate="customHtml"> | 77 | <ng-template ptTemplate="customHtml"> |
78 | <ng-container i18n> | 78 | <ng-container i18n> |
79 | <a href="https://chooser-beta.creativecommons.org/" target="_blank" rel="noopener noreferrer">Choose</a> the appropriate license for your work. | 79 | <a href="https://chooser-beta.creativecommons.org/" target="_blank" rel="noopener noreferrer">Choose</a> the appropriate licence for your work. |
80 | </ng-container> | 80 | </ng-container> |
81 | </ng-template> | 81 | </ng-template> |
82 | </my-help> | 82 | </my-help> |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss index bc32d7964..c1c7c686d 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss | |||
@@ -1,9 +1,3 @@ | |||
1 | // Bootstrap grid utilities require functions, variables and mixins | ||
2 | @import 'node_modules/bootstrap/scss/functions'; | ||
3 | @import 'node_modules/bootstrap/scss/variables'; | ||
4 | @import 'node_modules/bootstrap/scss/mixins'; | ||
5 | @import 'node_modules/bootstrap/scss/grid'; | ||
6 | |||
7 | @import 'variables'; | 1 | @import 'variables'; |
8 | @import 'mixins'; | 2 | @import 'mixins'; |
9 | 3 | ||
@@ -57,65 +51,62 @@ my-peertube-checkbox { | |||
57 | } | 51 | } |
58 | } | 52 | } |
59 | 53 | ||
60 | .captions { | 54 | .captions-header { |
61 | 55 | text-align: right; | |
62 | .captions-header { | 56 | margin-bottom: 1rem; |
63 | text-align: right; | 57 | } |
64 | margin-bottom: 1rem; | ||
65 | 58 | ||
66 | .create-caption { | 59 | .create-caption { |
67 | @include create-button; | 60 | @include create-button; |
68 | } | 61 | } |
69 | } | ||
70 | 62 | ||
71 | .caption-entry { | 63 | .caption-entry { |
72 | display: flex; | 64 | display: flex; |
73 | height: 40px; | 65 | height: 40px; |
74 | align-items: center; | 66 | align-items: center; |
75 | 67 | ||
76 | a.caption-entry-label { | 68 | a.caption-entry-label { |
77 | @include disable-default-a-behaviour; | 69 | @include disable-default-a-behaviour; |
78 | 70 | ||
79 | flex-grow: 1; | 71 | flex-grow: 1; |
80 | color: #000; | 72 | color: #000; |
81 | 73 | ||
82 | &:hover { | 74 | &:hover { |
83 | opacity: 0.8; | 75 | opacity: 0.8; |
84 | } | ||
85 | } | 76 | } |
77 | } | ||
86 | 78 | ||
87 | .caption-entry-label { | 79 | .caption-entry-label { |
88 | font-size: 15px; | 80 | font-size: 15px; |
89 | font-weight: bold; | 81 | font-weight: bold; |
90 | |||
91 | margin-right: 20px; | ||
92 | width: 150px; | ||
93 | } | ||
94 | 82 | ||
95 | .caption-entry-state { | 83 | margin-right: 20px; |
96 | width: 200px; | 84 | width: 150px; |
85 | } | ||
97 | 86 | ||
98 | &.caption-entry-state-create { | 87 | .caption-entry-state { |
99 | color: #39CC0B; | 88 | width: 200px; |
100 | } | ||
101 | 89 | ||
102 | &.caption-entry-state-delete { | 90 | &.caption-entry-state-create { |
103 | color: #FF0000; | 91 | color: #39CC0B; |
104 | } | ||
105 | } | 92 | } |
106 | 93 | ||
107 | .caption-entry-delete { | 94 | &.caption-entry-state-delete { |
108 | @include peertube-button; | 95 | color: #FF0000; |
109 | @include grey-button; | ||
110 | } | 96 | } |
111 | } | 97 | } |
112 | 98 | ||
113 | .no-caption { | 99 | .caption-entry-delete { |
114 | text-align: center; | 100 | @include peertube-button; |
115 | font-size: 15px; | 101 | @include grey-button; |
116 | } | 102 | } |
117 | } | 103 | } |
118 | 104 | ||
105 | .no-caption { | ||
106 | text-align: center; | ||
107 | font-size: 15px; | ||
108 | } | ||
109 | |||
119 | .submit-container { | 110 | .submit-container { |
120 | text-align: right; | 111 | text-align: right; |
121 | 112 | ||
@@ -143,35 +134,15 @@ p-calendar { | |||
143 | } | 134 | } |
144 | } | 135 | } |
145 | 136 | ||
146 | // columns for the video | 137 | .form-columns { |
147 | .col-video-edit { | 138 | display: grid; |
148 | @include make-col-ready(); | ||
149 | 139 | ||
150 | @include media-breakpoint-up(md) { | 140 | grid-template-columns: 66% 1fr; |
151 | @include make-col(7); | 141 | grid-gap: 30px; |
152 | |||
153 | + .col-video-edit { | ||
154 | @include make-col(5); | ||
155 | } | ||
156 | } | ||
157 | |||
158 | @include media-breakpoint-up(xl) { | ||
159 | @include make-col(8); | ||
160 | |||
161 | + .col-video-edit { | ||
162 | @include make-col(4); | ||
163 | } | ||
164 | } | ||
165 | } | 142 | } |
166 | 143 | ||
167 | :host-context(.expanded) { | 144 | @include on-small-main-col { |
168 | .col-video-edit { | 145 | .form-columns { |
169 | @include media-breakpoint-up(md) { | 146 | grid-template-columns: 1fr; |
170 | @include make-col(8); | ||
171 | |||
172 | + .col-video-edit { | ||
173 | @include make-col(4); | ||
174 | } | ||
175 | } | ||
176 | } | 147 | } |
177 | } | 148 | } |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts index 34119f7ab..3d916dbce 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts | |||
@@ -21,8 +21,15 @@ import { | |||
21 | import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' | 21 | import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' |
22 | import { InstanceService } from '@app/shared/shared-instance' | 22 | import { InstanceService } from '@app/shared/shared-instance' |
23 | import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' | 23 | import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' |
24 | import { LiveVideo, ServerConfig, VideoConstant, VideoDetails, VideoPrivacy } from '@shared/models' | 24 | import { |
25 | import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' | 25 | LiveVideo, |
26 | RegisterClientFormFieldOptions, | ||
27 | RegisterClientVideoFieldOptions, | ||
28 | ServerConfig, | ||
29 | VideoConstant, | ||
30 | VideoDetails, | ||
31 | VideoPrivacy | ||
32 | } from '@shared/models' | ||
26 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' | 33 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' |
27 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' | 34 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' |
28 | import { VideoEditType } from './video-edit.type' | 35 | import { VideoEditType } from './video-edit.type' |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts b/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts new file mode 100644 index 000000000..3392a0d8a --- /dev/null +++ b/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { objectToFormData } from '@app/helpers' | ||
2 | import { resolveUrl, UploaderX } from 'ngx-uploadx' | ||
3 | |||
4 | /** | ||
5 | * multipart/form-data uploader extending the UploaderX implementation of Google Resumable | ||
6 | * for use with multer | ||
7 | * | ||
8 | * @see https://github.com/kukhariev/ngx-uploadx/blob/637e258fe366b8095203f387a6101a230ee4f8e6/src/uploadx/lib/uploaderx.ts | ||
9 | * @example | ||
10 | * | ||
11 | * options: UploadxOptions = { | ||
12 | * uploaderClass: UploaderXFormData | ||
13 | * }; | ||
14 | */ | ||
15 | export class UploaderXFormData extends UploaderX { | ||
16 | |||
17 | async getFileUrl (): Promise<string> { | ||
18 | const headers = { | ||
19 | 'X-Upload-Content-Length': this.size.toString(), | ||
20 | 'X-Upload-Content-Type': this.file.type || 'application/octet-stream' | ||
21 | } | ||
22 | |||
23 | const previewfile = this.metadata.previewfile as any as File | ||
24 | delete this.metadata.previewfile | ||
25 | |||
26 | const data = objectToFormData(this.metadata) | ||
27 | if (previewfile !== undefined) { | ||
28 | data.append('previewfile', previewfile, previewfile.name) | ||
29 | data.append('thumbnailfile', previewfile, previewfile.name) | ||
30 | } | ||
31 | |||
32 | await this.request({ | ||
33 | method: 'POST', | ||
34 | body: data, | ||
35 | url: this.endpoint, | ||
36 | headers | ||
37 | }) | ||
38 | |||
39 | const location = this.getValueFromResponse('location') | ||
40 | if (!location) { | ||
41 | throw new Error('Invalid or missing Location header') | ||
42 | } | ||
43 | |||
44 | this.offset = this.responseStatus === 201 ? 0 : undefined | ||
45 | |||
46 | return resolveUrl(location, this.endpoint) | ||
47 | } | ||
48 | } | ||
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 3aae24732..23bd5ef76 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 | |||
@@ -5,7 +5,7 @@ import { scrollToTop } from '@app/helpers' | |||
5 | import { FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormValidatorService } from '@app/shared/shared-forms' |
6 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' | 6 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' |
7 | import { LoadingBarService } from '@ngx-loading-bar/core' | 7 | import { LoadingBarService } from '@ngx-loading-bar/core' |
8 | import { VideoPrivacy, VideoUpdate } from '@shared/models' | 8 | import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models' |
9 | import { hydrateFormFromVideo } from '../shared/video-edit-utils' | 9 | import { hydrateFormFromVideo } from '../shared/video-edit-utils' |
10 | import { VideoSend } from './video-send' | 10 | import { VideoSend } from './video-send' |
11 | 11 | ||
@@ -113,7 +113,13 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af | |||
113 | this.loadingBar.useRef().complete() | 113 | this.loadingBar.useRef().complete() |
114 | this.isImportingVideo = false | 114 | this.isImportingVideo = false |
115 | this.firstStepError.emit() | 115 | this.firstStepError.emit() |
116 | this.notifier.error(err.message) | 116 | |
117 | let message = err.message | ||
118 | if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) { | ||
119 | message = $localize`Torrents with only 1 file are supported.` | ||
120 | } | ||
121 | |||
122 | this.notifier.error(message) | ||
117 | } | 123 | } |
118 | ) | 124 | ) |
119 | } | 125 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html index 4c0b09894..86a779f8a 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html | |||
@@ -1,12 +1,17 @@ | |||
1 | <div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="setVideoFile($event)"> | 1 | <div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="onFileDropped($event)"> |
2 | <div class="first-step-block"> | 2 | <div class="first-step-block"> |
3 | <my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon> | 3 | <my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon> |
4 | 4 | ||
5 | <div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'"> | 5 | <div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'"> |
6 | <span i18n>Select the file to upload</span> | 6 | <span i18n>Select the file to upload</span> |
7 | <input | 7 | <input |
8 | aria-label="Select the file to upload" i18n-aria-label | 8 | aria-label="Select the file to upload" |
9 | #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" autofocus | 9 | i18n-aria-label |
10 | #videofileInput | ||
11 | [accept]="videoExtensions" | ||
12 | (change)="onFileChange($event)" | ||
13 | id="videofile" | ||
14 | type="file" | ||
10 | /> | 15 | /> |
11 | </div> | 16 | </div> |
12 | 17 | ||
@@ -41,7 +46,13 @@ | |||
41 | </div> | 46 | </div> |
42 | 47 | ||
43 | <div class="form-group upload-audio-button"> | 48 | <div class="form-group upload-audio-button"> |
44 | <my-button className="orange-button" i18n-label [label]="getAudioUploadLabel()" icon="upload" (click)="uploadFirstStep(true)"></my-button> | 49 | <my-button |
50 | className="orange-button" | ||
51 | [label]="getAudioUploadLabel()" | ||
52 | icon="upload" | ||
53 | (click)="uploadAudio()" | ||
54 | > | ||
55 | </my-button> | ||
45 | </div> | 56 | </div> |
46 | </ng-container> | 57 | </ng-container> |
47 | </div> | 58 | </div> |
@@ -64,6 +75,7 @@ | |||
64 | <span>{{ error }}</span> | 75 | <span>{{ error }}</span> |
65 | </div> | 76 | </div> |
66 | </div> | 77 | </div> |
78 | |||
67 | <div class="btn-group" role="group"> | 79 | <div class="btn-group" role="group"> |
68 | <input type="button" class="btn" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" /> | 80 | <input type="button" class="btn" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" /> |
69 | <input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" /> | 81 | <input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" /> |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss index 9549257f6..d9f348a70 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss | |||
@@ -47,8 +47,4 @@ | |||
47 | 47 | ||
48 | margin-left: 10px; | 48 | margin-left: 10px; |
49 | } | 49 | } |
50 | |||
51 | .btn-group > input:not(:first-child) { | ||
52 | margin-left: 0; | ||
53 | } | ||
54 | } | 50 | } |
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 effb37077..bca1b6eb6 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 | |||
@@ -1,15 +1,16 @@ | |||
1 | import { Subscription } from 'rxjs' | ||
2 | import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' | ||
3 | import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' |
4 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx' | ||
4 | import { UploaderXFormData } from './uploaderx-form-data' | ||
5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' | 5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' |
6 | import { scrollToTop, uploadErrorHandler } from '@app/helpers' | 6 | import { scrollToTop, genericUploadErrorHandler } from '@app/helpers' |
7 | import { FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormValidatorService } from '@app/shared/shared-forms' |
8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
9 | import { LoadingBarService } from '@ngx-loading-bar/core' | 9 | import { LoadingBarService } from '@ngx-loading-bar/core' |
10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
11 | import { VideoPrivacy } from '@shared/models' | 11 | import { VideoPrivacy } from '@shared/models' |
12 | import { VideoSend } from './video-send' | 12 | import { VideoSend } from './video-send' |
13 | import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http' | ||
13 | 14 | ||
14 | @Component({ | 15 | @Component({ |
15 | selector: 'my-video-upload', | 16 | selector: 'my-video-upload', |
@@ -20,23 +21,18 @@ import { VideoSend } from './video-send' | |||
20 | './video-send.scss' | 21 | './video-send.scss' |
21 | ] | 22 | ] |
22 | }) | 23 | }) |
23 | export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate { | 24 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, AfterViewInit, CanComponentDeactivate { |
24 | @Output() firstStepDone = new EventEmitter<string>() | 25 | @Output() firstStepDone = new EventEmitter<string>() |
25 | @Output() firstStepError = new EventEmitter<void>() | 26 | @Output() firstStepError = new EventEmitter<void>() |
26 | @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> | 27 | @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> |
27 | 28 | ||
28 | // So that it can be accessed in the template | ||
29 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY | ||
30 | |||
31 | userVideoQuotaUsed = 0 | 29 | userVideoQuotaUsed = 0 |
32 | userVideoQuotaUsedDaily = 0 | 30 | userVideoQuotaUsedDaily = 0 |
33 | 31 | ||
34 | isUploadingAudioFile = false | 32 | isUploadingAudioFile = false |
35 | isUploadingVideo = false | 33 | isUploadingVideo = false |
36 | isUpdatingVideo = false | ||
37 | 34 | ||
38 | videoUploaded = false | 35 | videoUploaded = false |
39 | videoUploadObservable: Subscription = null | ||
40 | videoUploadPercents = 0 | 36 | videoUploadPercents = 0 |
41 | videoUploadedIds = { | 37 | videoUploadedIds = { |
42 | id: 0, | 38 | id: 0, |
@@ -49,7 +45,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
49 | error: string | 45 | error: string |
50 | enableRetryAfterError: boolean | 46 | enableRetryAfterError: boolean |
51 | 47 | ||
48 | // So that it can be accessed in the template | ||
52 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC | 49 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC |
50 | protected readonly BASE_VIDEO_UPLOAD_URL = VideoService.BASE_VIDEO_URL + 'upload-resumable' | ||
51 | |||
52 | private uploadxOptions: UploadxOptions | ||
53 | private isUpdatingVideo = false | ||
54 | private fileToUpload: File | ||
53 | 55 | ||
54 | constructor ( | 56 | constructor ( |
55 | protected formValidatorService: FormValidatorService, | 57 | protected formValidatorService: FormValidatorService, |
@@ -61,15 +63,77 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
61 | protected videoCaptionService: VideoCaptionService, | 63 | protected videoCaptionService: VideoCaptionService, |
62 | private userService: UserService, | 64 | private userService: UserService, |
63 | private router: Router, | 65 | private router: Router, |
64 | private hooks: HooksService | 66 | private hooks: HooksService, |
65 | ) { | 67 | private resumableUploadService: UploadxService |
68 | ) { | ||
66 | super() | 69 | super() |
70 | |||
71 | this.uploadxOptions = { | ||
72 | endpoint: this.BASE_VIDEO_UPLOAD_URL, | ||
73 | multiple: false, | ||
74 | token: this.authService.getAccessToken(), | ||
75 | uploaderClass: UploaderXFormData, | ||
76 | retryConfig: { | ||
77 | maxAttempts: 6, | ||
78 | shouldRetry: (code: number) => { | ||
79 | return code < 400 || code >= 501 | ||
80 | } | ||
81 | } | ||
82 | } | ||
67 | } | 83 | } |
68 | 84 | ||
69 | get videoExtensions () { | 85 | get videoExtensions () { |
70 | return this.serverConfig.video.file.extensions.join(', ') | 86 | return this.serverConfig.video.file.extensions.join(', ') |
71 | } | 87 | } |
72 | 88 | ||
89 | onUploadVideoOngoing (state: UploadState) { | ||
90 | switch (state.status) { | ||
91 | case 'error': | ||
92 | const error = state.response?.error || 'Unknow error' | ||
93 | |||
94 | this.handleUploadError({ | ||
95 | error: new Error(error), | ||
96 | name: 'HttpErrorResponse', | ||
97 | message: error, | ||
98 | ok: false, | ||
99 | headers: new HttpHeaders(state.responseHeaders), | ||
100 | status: +state.responseStatus, | ||
101 | statusText: error, | ||
102 | type: HttpEventType.Response, | ||
103 | url: state.url | ||
104 | }) | ||
105 | break | ||
106 | |||
107 | case 'cancelled': | ||
108 | this.isUploadingVideo = false | ||
109 | this.videoUploadPercents = 0 | ||
110 | |||
111 | this.firstStepError.emit() | ||
112 | this.enableRetryAfterError = false | ||
113 | this.error = '' | ||
114 | break | ||
115 | |||
116 | case 'queue': | ||
117 | this.closeFirstStep(state.name) | ||
118 | break | ||
119 | |||
120 | case 'uploading': | ||
121 | this.videoUploadPercents = state.progress | ||
122 | break | ||
123 | |||
124 | case 'paused': | ||
125 | this.notifier.info($localize`Upload on hold`) | ||
126 | break | ||
127 | |||
128 | case 'complete': | ||
129 | this.videoUploaded = true | ||
130 | this.videoUploadPercents = 100 | ||
131 | |||
132 | this.videoUploadedIds = state?.response.video | ||
133 | break | ||
134 | } | ||
135 | } | ||
136 | |||
73 | ngOnInit () { | 137 | ngOnInit () { |
74 | super.ngOnInit() | 138 | super.ngOnInit() |
75 | 139 | ||
@@ -78,6 +142,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
78 | this.userVideoQuotaUsed = data.videoQuotaUsed | 142 | this.userVideoQuotaUsed = data.videoQuotaUsed |
79 | this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily | 143 | this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily |
80 | }) | 144 | }) |
145 | |||
146 | this.resumableUploadService.events | ||
147 | .subscribe(state => this.onUploadVideoOngoing(state)) | ||
81 | } | 148 | } |
82 | 149 | ||
83 | ngAfterViewInit () { | 150 | ngAfterViewInit () { |
@@ -85,7 +152,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
85 | } | 152 | } |
86 | 153 | ||
87 | ngOnDestroy () { | 154 | ngOnDestroy () { |
88 | if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() | 155 | this.cancelUpload() |
89 | } | 156 | } |
90 | 157 | ||
91 | canDeactivate () { | 158 | canDeactivate () { |
@@ -105,137 +172,43 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
105 | } | 172 | } |
106 | } | 173 | } |
107 | 174 | ||
108 | getVideoFile () { | 175 | onFileDropped (files: FileList) { |
109 | return this.videofileInput.nativeElement.files[0] | ||
110 | } | ||
111 | |||
112 | setVideoFile (files: FileList) { | ||
113 | this.videofileInput.nativeElement.files = files | 176 | this.videofileInput.nativeElement.files = files |
114 | this.fileChange() | ||
115 | } | ||
116 | |||
117 | getAudioUploadLabel () { | ||
118 | const videofile = this.getVideoFile() | ||
119 | if (!videofile) return $localize`Upload` | ||
120 | 177 | ||
121 | return $localize`Upload ${videofile.name}` | 178 | this.onFileChange({ target: this.videofileInput.nativeElement }) |
122 | } | 179 | } |
123 | 180 | ||
124 | fileChange () { | 181 | onFileChange (event: Event | { target: HTMLInputElement }) { |
125 | this.uploadFirstStep() | 182 | const file = (event.target as HTMLInputElement).files[0] |
126 | } | ||
127 | |||
128 | retryUpload () { | ||
129 | this.enableRetryAfterError = false | ||
130 | this.error = '' | ||
131 | this.uploadVideo() | ||
132 | } | ||
133 | |||
134 | cancelUpload () { | ||
135 | if (this.videoUploadObservable !== null) { | ||
136 | this.videoUploadObservable.unsubscribe() | ||
137 | } | ||
138 | |||
139 | this.isUploadingVideo = false | ||
140 | this.videoUploadPercents = 0 | ||
141 | this.videoUploadObservable = null | ||
142 | 183 | ||
143 | this.firstStepError.emit() | 184 | if (!file) return |
144 | this.enableRetryAfterError = false | ||
145 | this.error = '' | ||
146 | 185 | ||
147 | this.notifier.info($localize`Upload cancelled`) | 186 | if (!this.checkGlobalUserQuota(file)) return |
148 | } | 187 | if (!this.checkDailyUserQuota(file)) return |
149 | 188 | ||
150 | uploadFirstStep (clickedOnButton = false) { | 189 | if (this.isAudioFile(file.name)) { |
151 | const videofile = this.getVideoFile() | ||
152 | if (!videofile) return | ||
153 | |||
154 | if (!this.checkGlobalUserQuota(videofile)) return | ||
155 | if (!this.checkDailyUserQuota(videofile)) return | ||
156 | |||
157 | if (clickedOnButton === false && this.isAudioFile(videofile.name)) { | ||
158 | this.isUploadingAudioFile = true | 190 | this.isUploadingAudioFile = true |
159 | return | 191 | return |
160 | } | 192 | } |
161 | 193 | ||
162 | // Build name field | ||
163 | const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') | ||
164 | let name: string | ||
165 | |||
166 | // If the name of the file is very small, keep the extension | ||
167 | if (nameWithoutExtension.length < 3) name = videofile.name | ||
168 | else name = nameWithoutExtension | ||
169 | |||
170 | const nsfw = this.serverConfig.instance.isNSFW | ||
171 | const waitTranscoding = true | ||
172 | const commentsEnabled = true | ||
173 | const downloadEnabled = true | ||
174 | const channelId = this.firstStepChannelId.toString() | ||
175 | |||
176 | this.formData = new FormData() | ||
177 | this.formData.append('name', name) | ||
178 | // Put the video "private" -> we are waiting the user validation of the second step | ||
179 | this.formData.append('privacy', VideoPrivacy.PRIVATE.toString()) | ||
180 | this.formData.append('nsfw', '' + nsfw) | ||
181 | this.formData.append('commentsEnabled', '' + commentsEnabled) | ||
182 | this.formData.append('downloadEnabled', '' + downloadEnabled) | ||
183 | this.formData.append('waitTranscoding', '' + waitTranscoding) | ||
184 | this.formData.append('channelId', '' + channelId) | ||
185 | this.formData.append('videofile', videofile) | ||
186 | |||
187 | if (this.previewfileUpload) { | ||
188 | this.formData.append('previewfile', this.previewfileUpload) | ||
189 | this.formData.append('thumbnailfile', this.previewfileUpload) | ||
190 | } | ||
191 | |||
192 | this.isUploadingVideo = true | 194 | this.isUploadingVideo = true |
193 | this.firstStepDone.emit(name) | 195 | this.fileToUpload = file |
194 | |||
195 | this.form.patchValue({ | ||
196 | name, | ||
197 | privacy: this.firstStepPrivacyId, | ||
198 | nsfw, | ||
199 | channelId: this.firstStepChannelId, | ||
200 | previewfile: this.previewfileUpload | ||
201 | }) | ||
202 | 196 | ||
203 | this.uploadVideo() | 197 | this.uploadFile(file) |
204 | } | 198 | } |
205 | 199 | ||
206 | uploadVideo () { | 200 | uploadAudio () { |
207 | this.videoUploadObservable = this.videoService.uploadVideo(this.formData).subscribe( | 201 | this.uploadFile(this.getInputVideoFile(), this.previewfileUpload) |
208 | event => { | 202 | } |
209 | if (event.type === HttpEventType.UploadProgress) { | ||
210 | this.videoUploadPercents = Math.round(100 * event.loaded / event.total) | ||
211 | } else if (event instanceof HttpResponse) { | ||
212 | this.videoUploaded = true | ||
213 | |||
214 | this.videoUploadedIds = event.body.video | ||
215 | |||
216 | this.videoUploadObservable = null | ||
217 | } | ||
218 | }, | ||
219 | 203 | ||
220 | (err: HttpErrorResponse) => { | 204 | retryUpload () { |
221 | // Reset progress (but keep isUploadingVideo true) | 205 | this.enableRetryAfterError = false |
222 | this.videoUploadPercents = 0 | 206 | this.error = '' |
223 | this.videoUploadObservable = null | 207 | this.uploadFile(this.fileToUpload) |
224 | this.enableRetryAfterError = true | 208 | } |
225 | |||
226 | this.error = uploadErrorHandler({ | ||
227 | err, | ||
228 | name: $localize`video`, | ||
229 | notifier: this.notifier, | ||
230 | sticky: false | ||
231 | }) | ||
232 | 209 | ||
233 | if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413 || | 210 | cancelUpload () { |
234 | err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { | 211 | this.resumableUploadService.control({ action: 'cancel' }) |
235 | this.cancelUpload() | ||
236 | } | ||
237 | } | ||
238 | ) | ||
239 | } | 212 | } |
240 | 213 | ||
241 | isPublishingButtonDisabled () { | 214 | isPublishingButtonDisabled () { |
@@ -245,6 +218,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
245 | !this.videoUploadedIds.id | 218 | !this.videoUploadedIds.id |
246 | } | 219 | } |
247 | 220 | ||
221 | getAudioUploadLabel () { | ||
222 | const videofile = this.getInputVideoFile() | ||
223 | if (!videofile) return $localize`Upload` | ||
224 | |||
225 | return $localize`Upload ${videofile.name}` | ||
226 | } | ||
227 | |||
248 | updateSecondStep () { | 228 | updateSecondStep () { |
249 | if (this.isPublishingButtonDisabled() || !this.checkForm()) { | 229 | if (this.isPublishingButtonDisabled() || !this.checkForm()) { |
250 | return | 230 | return |
@@ -275,6 +255,62 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
275 | ) | 255 | ) |
276 | } | 256 | } |
277 | 257 | ||
258 | private getInputVideoFile () { | ||
259 | return this.videofileInput.nativeElement.files[0] | ||
260 | } | ||
261 | |||
262 | private uploadFile (file: File, previewfile?: File) { | ||
263 | const metadata = { | ||
264 | waitTranscoding: true, | ||
265 | commentsEnabled: true, | ||
266 | downloadEnabled: true, | ||
267 | channelId: this.firstStepChannelId, | ||
268 | nsfw: this.serverConfig.instance.isNSFW, | ||
269 | privacy: VideoPrivacy.PRIVATE.toString(), | ||
270 | filename: file.name, | ||
271 | previewfile: previewfile as any | ||
272 | } | ||
273 | |||
274 | this.resumableUploadService.handleFiles(file, { | ||
275 | ...this.uploadxOptions, | ||
276 | metadata | ||
277 | }) | ||
278 | |||
279 | this.isUploadingVideo = true | ||
280 | } | ||
281 | |||
282 | private handleUploadError (err: HttpErrorResponse) { | ||
283 | // Reset progress (but keep isUploadingVideo true) | ||
284 | this.videoUploadPercents = 0 | ||
285 | this.enableRetryAfterError = true | ||
286 | |||
287 | this.error = genericUploadErrorHandler({ | ||
288 | err, | ||
289 | name: $localize`video`, | ||
290 | notifier: this.notifier, | ||
291 | sticky: false | ||
292 | }) | ||
293 | |||
294 | if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { | ||
295 | this.cancelUpload() | ||
296 | } | ||
297 | } | ||
298 | |||
299 | private closeFirstStep (filename: string) { | ||
300 | const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '') | ||
301 | const name = nameWithoutExtension.length < 3 ? filename : nameWithoutExtension | ||
302 | |||
303 | this.form.patchValue({ | ||
304 | name, | ||
305 | privacy: this.firstStepPrivacyId, | ||
306 | nsfw: this.serverConfig.instance.isNSFW, | ||
307 | channelId: this.firstStepChannelId, | ||
308 | previewfile: this.previewfileUpload | ||
309 | }) | ||
310 | |||
311 | this.firstStepDone.emit(name) | ||
312 | } | ||
313 | |||
278 | private checkGlobalUserQuota (videofile: File) { | 314 | private checkGlobalUserQuota (videofile: File) { |
279 | const bytePipes = new BytesPipe() | 315 | const bytePipes = new BytesPipe() |
280 | 316 | ||
@@ -285,8 +321,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
285 | const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0) | 321 | const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0) |
286 | const videoQuotaBytes = bytePipes.transform(videoQuota, 0) | 322 | const videoQuotaBytes = bytePipes.transform(videoQuota, 0) |
287 | 323 | ||
288 | const msg = $localize`Your video quota is exceeded with this video ( | 324 | const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` |
289 | video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` | ||
290 | this.notifier.error(msg) | 325 | this.notifier.error(msg) |
291 | 326 | ||
292 | return false | 327 | return false |
@@ -304,9 +339,7 @@ video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuota | |||
304 | const videoSizeBytes = bytePipes.transform(videofile.size, 0) | 339 | const videoSizeBytes = bytePipes.transform(videofile.size, 0) |
305 | const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0) | 340 | const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0) |
306 | const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0) | 341 | const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0) |
307 | 342 | const msg = $localize`Your daily video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})` | |
308 | const msg = $localize`Your daily video quota is exceeded with this video ( | ||
309 | video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})` | ||
310 | this.notifier.error(msg) | 343 | this.notifier.error(msg) |
311 | 344 | ||
312 | return false | 345 | return false |
diff --git a/client/src/app/+videos/+video-edit/video-add-routing.module.ts b/client/src/app/+videos/+video-edit/video-add-routing.module.ts index 9ff66bea0..3b9a5ab3a 100644 --- a/client/src/app/+videos/+video-edit/video-add-routing.module.ts +++ b/client/src/app/+videos/+video-edit/video-add-routing.module.ts | |||
@@ -1,14 +1,13 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { CanDeactivateGuard, LoginGuard } from '@app/core' | 3 | import { CanDeactivateGuard, LoginGuard } from '@app/core' |
4 | import { MetaGuard } from '@ngx-meta/core' | ||
5 | import { VideoAddComponent } from './video-add.component' | 4 | import { VideoAddComponent } from './video-add.component' |
6 | 5 | ||
7 | const videoAddRoutes: Routes = [ | 6 | const videoAddRoutes: Routes = [ |
8 | { | 7 | { |
9 | path: '', | 8 | path: '', |
10 | component: VideoAddComponent, | 9 | component: VideoAddComponent, |
11 | canActivate: [ MetaGuard, LoginGuard ], | 10 | canActivate: [ LoginGuard ], |
12 | canDeactivate: [ CanDeactivateGuard ] | 11 | canDeactivate: [ CanDeactivateGuard ] |
13 | } | 12 | } |
14 | ] | 13 | ] |
diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html index dc8c2f21d..ac75d9ff8 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.html +++ b/client/src/app/+videos/+video-edit/video-add.component.html | |||
@@ -20,8 +20,8 @@ | |||
20 | <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> | 20 | <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> |
21 | </div> | 21 | </div> |
22 | 22 | ||
23 | <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [ngClass]="{ 'hide-nav': secondStepType !== undefined }"> | 23 | <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }"> |
24 | <ng-container ngbNavItem> | 24 | <ng-container ngbNavItem="upload"> |
25 | <a ngbNavLink> | 25 | <a ngbNavLink> |
26 | <span i18n>Upload a file</span> | 26 | <span i18n>Upload a file</span> |
27 | </a> | 27 | </a> |
@@ -31,7 +31,7 @@ | |||
31 | </ng-template> | 31 | </ng-template> |
32 | </ng-container> | 32 | </ng-container> |
33 | 33 | ||
34 | <ng-container ngbNavItem *ngIf="isVideoImportHttpEnabled()"> | 34 | <ng-container ngbNavItem="import-url" *ngIf="isVideoImportHttpEnabled()"> |
35 | <a ngbNavLink> | 35 | <a ngbNavLink> |
36 | <span i18n>Import with URL</span> | 36 | <span i18n>Import with URL</span> |
37 | </a> | 37 | </a> |
@@ -41,7 +41,7 @@ | |||
41 | </ng-template> | 41 | </ng-template> |
42 | </ng-container> | 42 | </ng-container> |
43 | 43 | ||
44 | <ng-container ngbNavItem *ngIf="isVideoImportTorrentEnabled()"> | 44 | <ng-container ngbNavItem="import-torrent" *ngIf="isVideoImportTorrentEnabled()"> |
45 | <a ngbNavLink> | 45 | <a ngbNavLink> |
46 | <span i18n>Import with torrent</span> | 46 | <span i18n>Import with torrent</span> |
47 | </a> | 47 | </a> |
@@ -51,7 +51,7 @@ | |||
51 | </ng-template> | 51 | </ng-template> |
52 | </ng-container> | 52 | </ng-container> |
53 | 53 | ||
54 | <ng-container ngbNavItem *ngIf="isVideoLiveEnabled()"> | 54 | <ng-container ngbNavItem="go-live" *ngIf="isVideoLiveEnabled()"> |
55 | <a ngbNavLink> | 55 | <a ngbNavLink> |
56 | <span i18n>Go live</span> | 56 | <span i18n>Go live</span> |
57 | </a> | 57 | </a> |
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 441d5a3db..d735c936c 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.ts +++ b/client/src/app/+videos/+video-edit/video-add.component.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Component, HostListener, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, HostListener, OnInit, ViewChild } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
2 | import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core' | 3 | import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core' |
3 | import { ServerConfig } from '@shared/models' | 4 | import { ServerConfig } from '@shared/models' |
4 | import { VideoEditType } from './shared/video-edit.type' | 5 | import { VideoEditType } from './shared/video-edit.type' |
@@ -22,11 +23,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
22 | 23 | ||
23 | secondStepType: VideoEditType | 24 | secondStepType: VideoEditType |
24 | videoName: string | 25 | videoName: string |
25 | serverConfig: ServerConfig | 26 | |
27 | activeNav: string | ||
28 | |||
29 | private serverConfig: ServerConfig | ||
26 | 30 | ||
27 | constructor ( | 31 | constructor ( |
28 | private auth: AuthService, | 32 | private auth: AuthService, |
29 | private serverService: ServerService | 33 | private serverService: ServerService, |
34 | private route: ActivatedRoute, | ||
35 | private router: Router | ||
30 | ) {} | 36 | ) {} |
31 | 37 | ||
32 | get userInformationLoaded () { | 38 | get userInformationLoaded () { |
@@ -42,6 +48,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
42 | .subscribe(config => this.serverConfig = config) | 48 | .subscribe(config => this.serverConfig = config) |
43 | 49 | ||
44 | this.user = this.auth.getUser() | 50 | this.user = this.auth.getUser() |
51 | |||
52 | if (this.route.snapshot.fragment) { | ||
53 | this.onNavChange(this.route.snapshot.fragment) | ||
54 | } | ||
55 | } | ||
56 | |||
57 | onNavChange (newActiveNav: string) { | ||
58 | this.activeNav = newActiveNav | ||
59 | |||
60 | this.router.navigate([], { fragment: this.activeNav }) | ||
45 | } | 61 | } |
46 | 62 | ||
47 | onFirstStepDone (type: VideoEditType, videoName: string) { | 63 | onFirstStepDone (type: VideoEditType, videoName: string) { |
diff --git a/client/src/app/+videos/+video-edit/video-add.module.ts b/client/src/app/+videos/+video-edit/video-add.module.ts index da651119b..e836cf81e 100644 --- a/client/src/app/+videos/+video-edit/video-add.module.ts +++ b/client/src/app/+videos/+video-edit/video-add.module.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { CanDeactivateGuard } from '@app/core' | 2 | import { CanDeactivateGuard } from '@app/core' |
3 | import { UploadxModule } from 'ngx-uploadx' | ||
3 | import { VideoEditModule } from './shared/video-edit.module' | 4 | import { VideoEditModule } from './shared/video-edit.module' |
4 | import { DragDropDirective } from './video-add-components/drag-drop.directive' | 5 | import { DragDropDirective } from './video-add-components/drag-drop.directive' |
5 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' | 6 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' |
@@ -13,7 +14,9 @@ import { VideoAddComponent } from './video-add.component' | |||
13 | imports: [ | 14 | imports: [ |
14 | VideoAddRoutingModule, | 15 | VideoAddRoutingModule, |
15 | 16 | ||
16 | VideoEditModule | 17 | VideoEditModule, |
18 | |||
19 | UploadxModule | ||
17 | ], | 20 | ], |
18 | 21 | ||
19 | declarations: [ | 22 | declarations: [ |
diff --git a/client/src/app/+videos/+video-edit/video-update-routing.module.ts b/client/src/app/+videos/+video-edit/video-update-routing.module.ts index a04351b05..ba9167dd0 100644 --- a/client/src/app/+videos/+video-edit/video-update-routing.module.ts +++ b/client/src/app/+videos/+video-edit/video-update-routing.module.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { CanDeactivateGuard, LoginGuard } from '@app/core' | 3 | import { CanDeactivateGuard, LoginGuard } from '@app/core' |
4 | import { MetaGuard } from '@ngx-meta/core' | ||
5 | import { VideoUpdateComponent } from './video-update.component' | 4 | import { VideoUpdateComponent } from './video-update.component' |
6 | import { VideoUpdateResolver } from './video-update.resolver' | 5 | import { VideoUpdateResolver } from './video-update.resolver' |
7 | 6 | ||
@@ -9,7 +8,7 @@ const videoUpdateRoutes: Routes = [ | |||
9 | { | 8 | { |
10 | path: '', | 9 | path: '', |
11 | component: VideoUpdateComponent, | 10 | component: VideoUpdateComponent, |
12 | canActivate: [ MetaGuard, LoginGuard ], | 11 | canActivate: [ LoginGuard ], |
13 | canDeactivate: [ CanDeactivateGuard ], | 12 | canDeactivate: [ CanDeactivateGuard ], |
14 | resolve: { | 13 | resolve: { |
15 | videoData: VideoUpdateResolver | 14 | videoData: VideoUpdateResolver |
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts index 276548b79..9172b78a8 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts | |||
@@ -2,7 +2,9 @@ import { forkJoin, of } from 'rxjs' | |||
2 | import { map, switchMap } from 'rxjs/operators' | 2 | import { map, switchMap } from 'rxjs/operators' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' | 4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' |
5 | import { VideoCaptionService, VideoChannelService, VideoDetails, VideoService } from '@app/shared/shared-main' | 5 | import { AuthService } from '@app/core' |
6 | import { listUserChannels } from '@app/helpers' | ||
7 | import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | ||
6 | import { LiveVideoService } from '@app/shared/shared-video-live' | 8 | import { LiveVideoService } from '@app/shared/shared-video-live' |
7 | 9 | ||
8 | @Injectable() | 10 | @Injectable() |
@@ -10,7 +12,7 @@ export class VideoUpdateResolver implements Resolve<any> { | |||
10 | constructor ( | 12 | constructor ( |
11 | private videoService: VideoService, | 13 | private videoService: VideoService, |
12 | private liveVideoService: LiveVideoService, | 14 | private liveVideoService: LiveVideoService, |
13 | private videoChannelService: VideoChannelService, | 15 | private authService: AuthService, |
14 | private videoCaptionService: VideoCaptionService | 16 | private videoCaptionService: VideoCaptionService |
15 | ) { | 17 | ) { |
16 | } | 18 | } |
@@ -31,17 +33,7 @@ export class VideoUpdateResolver implements Resolve<any> { | |||
31 | .loadCompleteDescription(video.descriptionPath) | 33 | .loadCompleteDescription(video.descriptionPath) |
32 | .pipe(map(description => Object.assign(video, { description }))), | 34 | .pipe(map(description => Object.assign(video, { description }))), |
33 | 35 | ||
34 | this.videoChannelService | 36 | listUserChannels(this.authService), |
35 | .listAccountVideoChannels(video.account) | ||
36 | .pipe( | ||
37 | map(result => result.data), | ||
38 | map(videoChannels => videoChannels.map(c => ({ | ||
39 | id: c.id, | ||
40 | label: c.displayName, | ||
41 | support: c.support, | ||
42 | avatarPath: c.avatar?.path | ||
43 | }))) | ||
44 | ), | ||
45 | 37 | ||
46 | this.videoCaptionService | 38 | this.videoCaptionService |
47 | .listCaptions(video.id) | 39 | .listCaptions(video.id) |
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 fd379e80e..04f8f0d58 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 | |||
@@ -161,7 +161,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
161 | // Before HTML rendering restore line feed for markdown list compatibility | 161 | // Before HTML rendering restore line feed for markdown list compatibility |
162 | const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n') | 162 | const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n') |
163 | const html = await this.markdownService.textMarkdownToHTML(commentText, true, true) | 163 | const html = await this.markdownService.textMarkdownToHTML(commentText, true, true) |
164 | this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html) | 164 | this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(html) |
165 | this.newParentComments = this.parentComments.concat([ this.comment ]) | 165 | this.newParentComments = this.parentComments.concat([ this.comment ]) |
166 | 166 | ||
167 | if (this.comment.account) { | 167 | if (this.comment.account) { |
diff --git a/client/src/app/+videos/+video-watch/video-watch-routing.module.ts b/client/src/app/+videos/+video-watch/video-watch-routing.module.ts index d8fecb87d..cb77685c0 100644 --- a/client/src/app/+videos/+video-watch/video-watch-routing.module.ts +++ b/client/src/app/+videos/+video-watch/video-watch-routing.module.ts | |||
@@ -1,13 +1,11 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { VideoWatchComponent } from './video-watch.component' | 3 | import { VideoWatchComponent } from './video-watch.component' |
5 | 4 | ||
6 | const videoWatchRoutes: Routes = [ | 5 | const videoWatchRoutes: Routes = [ |
7 | { | 6 | { |
8 | path: 'playlist/:playlistId', | 7 | path: 'playlist/:playlistId', |
9 | component: VideoWatchComponent, | 8 | component: VideoWatchComponent |
10 | canActivate: [ MetaGuard ] | ||
11 | }, | 9 | }, |
12 | { | 10 | { |
13 | path: ':videoId/comments/:commentId', | 11 | path: ':videoId/comments/:commentId', |
@@ -15,8 +13,7 @@ const videoWatchRoutes: Routes = [ | |||
15 | }, | 13 | }, |
16 | { | 14 | { |
17 | path: ':videoId', | 15 | path: ':videoId', |
18 | component: VideoWatchComponent, | 16 | component: VideoWatchComponent |
19 | canActivate: [ MetaGuard ] | ||
20 | } | 17 | } |
21 | ] | 18 | ] |
22 | 19 | ||
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.scss b/client/src/app/+videos/+video-watch/video-watch.component.scss index 301762695..6124090c9 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.scss +++ b/client/src/app/+videos/+video-watch/video-watch.component.scss | |||
@@ -146,6 +146,8 @@ $video-info-margin-left: 44px; | |||
146 | } | 146 | } |
147 | 147 | ||
148 | .video-info-name { | 148 | .video-info-name { |
149 | @include peertube-word-wrap; | ||
150 | |||
149 | margin-right: 30px; | 151 | margin-right: 30px; |
150 | min-height: 40px; // Align with the action buttons | 152 | min-height: 40px; // Align with the action buttons |
151 | font-size: 27px; | 153 | font-size: 27px; |
@@ -173,6 +175,7 @@ $video-info-margin-left: 44px; | |||
173 | 175 | ||
174 | a { | 176 | a { |
175 | @include disable-default-a-behaviour; | 177 | @include disable-default-a-behaviour; |
178 | @include peertube-word-wrap; | ||
176 | 179 | ||
177 | color: pvar(--mainForegroundColor); | 180 | color: pvar(--mainForegroundColor); |
178 | 181 | ||
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 1c510d6b0..88c5cef52 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -9,6 +9,7 @@ import { | |||
9 | AuthUser, | 9 | AuthUser, |
10 | ConfirmService, | 10 | ConfirmService, |
11 | MarkdownService, | 11 | MarkdownService, |
12 | MetaService, | ||
12 | Notifier, | 13 | Notifier, |
13 | PeerTubeSocket, | 14 | PeerTubeSocket, |
14 | RestExtractor, | 15 | RestExtractor, |
@@ -25,7 +26,6 @@ import { SupportModalComponent } from '@app/shared/shared-support-modal' | |||
25 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 26 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
26 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' | 27 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' |
27 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 28 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
28 | import { MetaService } from '@ngx-meta/core' | ||
29 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' | 29 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' |
30 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 30 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
31 | import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' | 31 | import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' |
@@ -509,7 +509,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
509 | 509 | ||
510 | private async setVideoDescriptionHTML () { | 510 | private async setVideoDescriptionHTML () { |
511 | const html = await this.markdownService.textMarkdownToHTML(this.video.description) | 511 | const html = await this.markdownService.textMarkdownToHTML(this.video.description) |
512 | this.videoHTMLDescription = await this.markdownService.processVideoTimestamps(html) | 512 | this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html) |
513 | } | 513 | } |
514 | 514 | ||
515 | private setVideoLikesBarTooltipText () { | 515 | private setVideoLikesBarTooltipText () { |
@@ -674,7 +674,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
674 | 674 | ||
675 | this.player.one('ended', () => { | 675 | this.player.one('ended', () => { |
676 | if (this.video.isLive) { | 676 | if (this.video.isLive) { |
677 | this.video.state.id = VideoState.LIVE_ENDED | 677 | this.zone.run(() => this.video.state.id = VideoState.LIVE_ENDED) |
678 | } | 678 | } |
679 | }) | 679 | }) |
680 | 680 | ||
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts index 55040f3c9..bbb02a236 100644 --- a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts +++ b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts | |||
@@ -31,7 +31,8 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple | |||
31 | private route: ActivatedRoute, | 31 | private route: ActivatedRoute, |
32 | private router: Router, | 32 | private router: Router, |
33 | private auth: AuthService, | 33 | private auth: AuthService, |
34 | private serverService: ServerService | 34 | private serverService: ServerService, |
35 | private redirectService: RedirectService | ||
35 | ) { | 36 | ) { |
36 | super(data) | 37 | super(data) |
37 | 38 | ||
@@ -84,12 +85,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple | |||
84 | 85 | ||
85 | this.algorithmChangeSub = this.route.queryParams.subscribe( | 86 | this.algorithmChangeSub = this.route.queryParams.subscribe( |
86 | queryParams => { | 87 | queryParams => { |
87 | const algorithm = queryParams['alg'] | 88 | this.data.model = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm() |
88 | if (algorithm) { | ||
89 | this.data.model = algorithm | ||
90 | } else { | ||
91 | this.data.model = RedirectService.DEFAULT_TRENDING_ALGORITHM | ||
92 | } | ||
93 | } | 89 | } |
94 | ) | 90 | ) |
95 | } | 91 | } |
@@ -99,7 +95,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple | |||
99 | } | 95 | } |
100 | 96 | ||
101 | setSort () { | 97 | setSort () { |
102 | const alg = this.data.model !== RedirectService.DEFAULT_TRENDING_ALGORITHM | 98 | const alg = this.data.model !== this.redirectService.getDefaultTrendingAlgorithm() |
103 | ? this.data.model | 99 | ? this.data.model |
104 | : undefined | 100 | : undefined |
105 | 101 | ||
diff --git a/client/src/app/+videos/video-list/trending/video-trending.component.ts b/client/src/app/+videos/video-list/trending/video-trending.component.ts index e50d6ec3a..ebec672f3 100644 --- a/client/src/app/+videos/video-list/trending/video-trending.component.ts +++ b/client/src/app/+videos/video-list/trending/video-trending.component.ts | |||
@@ -35,11 +35,12 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, | |||
35 | protected storageService: LocalStorageService, | 35 | protected storageService: LocalStorageService, |
36 | protected cfr: ComponentFactoryResolver, | 36 | protected cfr: ComponentFactoryResolver, |
37 | private videoService: VideoService, | 37 | private videoService: VideoService, |
38 | private redirectService: RedirectService, | ||
38 | private hooks: HooksService | 39 | private hooks: HooksService |
39 | ) { | 40 | ) { |
40 | super() | 41 | super() |
41 | 42 | ||
42 | this.defaultSort = this.parseAlgorithm(RedirectService.DEFAULT_TRENDING_ALGORITHM) | 43 | this.defaultSort = this.parseAlgorithm(this.redirectService.getDefaultTrendingAlgorithm()) |
43 | 44 | ||
44 | this.headerComponentInjector = this.getInjector() | 45 | this.headerComponentInjector = this.getInjector() |
45 | } | 46 | } |
@@ -106,7 +107,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, | |||
106 | } | 107 | } |
107 | 108 | ||
108 | protected loadPageRouteParams (queryParams: Params) { | 109 | protected loadPageRouteParams (queryParams: Params) { |
109 | const algorithm = queryParams['alg'] || RedirectService.DEFAULT_TRENDING_ALGORITHM | 110 | const algorithm = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm() |
110 | 111 | ||
111 | this.sort = this.parseAlgorithm(algorithm) | 112 | this.sort = this.parseAlgorithm(algorithm) |
112 | } | 113 | } |
@@ -115,8 +116,10 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, | |||
115 | switch (algorithm) { | 116 | switch (algorithm) { |
116 | case 'most-viewed': | 117 | case 'most-viewed': |
117 | return '-trending' | 118 | return '-trending' |
119 | |||
118 | case 'most-liked': | 120 | case 'most-liked': |
119 | return '-likes' | 121 | return '-likes' |
122 | |||
120 | default: | 123 | default: |
121 | return '-' + algorithm as VideoSortField | 124 | return '-' + algorithm as VideoSortField |
122 | } | 125 | } |
diff --git a/client/src/app/+videos/videos-routing.module.ts b/client/src/app/+videos/videos-routing.module.ts index 16e3b9bb2..f9f476b18 100644 --- a/client/src/app/+videos/videos-routing.module.ts +++ b/client/src/app/+videos/videos-routing.module.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { LoginGuard } from '@app/core' | 3 | import { LoginGuard } from '@app/core' |
4 | import { MetaGuard } from '@ngx-meta/core' | ||
5 | import { VideoTrendingComponent } from './video-list' | 4 | import { VideoTrendingComponent } from './video-list' |
6 | import { VideoOverviewComponent } from './video-list/overview/video-overview.component' | 5 | import { VideoOverviewComponent } from './video-list/overview/video-overview.component' |
7 | import { VideoLocalComponent } from './video-list/video-local.component' | 6 | import { VideoLocalComponent } from './video-list/video-local.component' |
@@ -13,7 +12,6 @@ const videosRoutes: Routes = [ | |||
13 | { | 12 | { |
14 | path: '', | 13 | path: '', |
15 | component: VideosComponent, | 14 | component: VideosComponent, |
16 | canActivateChild: [ MetaGuard ], | ||
17 | children: [ | 15 | children: [ |
18 | { | 16 | { |
19 | path: 'overview', | 17 | path: 'overview', |
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 0a43ab0ad..4619c4046 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts | |||
@@ -3,7 +3,7 @@ import { RouteReuseStrategy, RouterModule, Routes, UrlMatchResult, UrlSegment } | |||
3 | import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' | 3 | import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' |
4 | import { MenuGuards } from '@app/core/routing/menu-guard.service' | 4 | import { MenuGuards } from '@app/core/routing/menu-guard.service' |
5 | import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' | 5 | import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' |
6 | import { PreloadSelectedModulesList } from './core' | 6 | import { MetaGuard, PreloadSelectedModulesList } from './core' |
7 | import { EmptyComponent } from './empty.component' | 7 | import { EmptyComponent } from './empty.component' |
8 | import { RootComponent } from './root.component' | 8 | import { RootComponent } from './root.component' |
9 | 9 | ||
@@ -12,55 +12,72 @@ const routes: Routes = [ | |||
12 | path: 'admin', | 12 | path: 'admin', |
13 | canActivate: [ MenuGuards.close() ], | 13 | canActivate: [ MenuGuards.close() ], |
14 | canDeactivate: [ MenuGuards.open() ], | 14 | canDeactivate: [ MenuGuards.open() ], |
15 | loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule) | 15 | loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule), |
16 | canActivateChild: [ MetaGuard ] | ||
17 | }, | ||
18 | { | ||
19 | path: 'home', | ||
20 | loadChildren: () => import('./+home/home.module').then(m => m.HomeModule) | ||
16 | }, | 21 | }, |
17 | { | 22 | { |
18 | path: 'my-account', | 23 | path: 'my-account', |
19 | loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule) | 24 | loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule), |
25 | canActivateChild: [ MetaGuard ] | ||
20 | }, | 26 | }, |
21 | { | 27 | { |
22 | path: 'my-library', | 28 | path: 'my-library', |
23 | loadChildren: () => import('./+my-library/my-library.module').then(m => m.MyLibraryModule) | 29 | loadChildren: () => import('./+my-library/my-library.module').then(m => m.MyLibraryModule), |
30 | canActivateChild: [ MetaGuard ] | ||
24 | }, | 31 | }, |
25 | { | 32 | { |
26 | path: 'verify-account', | 33 | path: 'verify-account', |
27 | loadChildren: () => import('./+signup/+verify-account/verify-account.module').then(m => m.VerifyAccountModule) | 34 | loadChildren: () => import('./+signup/+verify-account/verify-account.module').then(m => m.VerifyAccountModule), |
35 | canActivateChild: [ MetaGuard ] | ||
28 | }, | 36 | }, |
29 | { | 37 | { |
30 | path: 'a', | 38 | path: 'a', |
31 | loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule) | 39 | loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule), |
40 | canActivateChild: [ MetaGuard ] | ||
32 | }, | 41 | }, |
33 | { | 42 | { |
34 | path: 'c', | 43 | path: 'c', |
35 | loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule) | 44 | loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule), |
45 | canActivateChild: [ MetaGuard ] | ||
36 | }, | 46 | }, |
37 | { | 47 | { |
38 | path: 'about', | 48 | path: 'about', |
39 | loadChildren: () => import('./+about/about.module').then(m => m.AboutModule) | 49 | loadChildren: () => import('./+about/about.module').then(m => m.AboutModule), |
50 | canActivateChild: [ MetaGuard ] | ||
40 | }, | 51 | }, |
41 | { | 52 | { |
42 | path: 'signup', | 53 | path: 'signup', |
43 | loadChildren: () => import('./+signup/+register/register.module').then(m => m.RegisterModule) | 54 | loadChildren: () => import('./+signup/+register/register.module').then(m => m.RegisterModule), |
55 | canActivateChild: [ MetaGuard ] | ||
44 | }, | 56 | }, |
45 | { | 57 | { |
46 | path: 'reset-password', | 58 | path: 'reset-password', |
47 | loadChildren: () => import('./+reset-password/reset-password.module').then(m => m.ResetPasswordModule) | 59 | loadChildren: () => import('./+reset-password/reset-password.module').then(m => m.ResetPasswordModule), |
60 | canActivateChild: [ MetaGuard ] | ||
48 | }, | 61 | }, |
49 | { | 62 | { |
50 | path: 'login', | 63 | path: 'login', |
51 | loadChildren: () => import('./+login/login.module').then(m => m.LoginModule) | 64 | loadChildren: () => import('./+login/login.module').then(m => m.LoginModule), |
65 | canActivateChild: [ MetaGuard ] | ||
52 | }, | 66 | }, |
53 | { | 67 | { |
54 | path: 'search', | 68 | path: 'search', |
55 | loadChildren: () => import('./+search/search.module').then(m => m.SearchModule) | 69 | loadChildren: () => import('./+search/search.module').then(m => m.SearchModule), |
70 | canActivateChild: [ MetaGuard ] | ||
56 | }, | 71 | }, |
57 | { | 72 | { |
58 | path: 'videos', | 73 | path: 'videos', |
59 | loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule) | 74 | loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule), |
75 | canActivateChild: [ MetaGuard ] | ||
60 | }, | 76 | }, |
61 | { | 77 | { |
62 | path: 'remote-interaction', | 78 | path: 'remote-interaction', |
63 | loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule) | 79 | loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule), |
80 | canActivateChild: [ MetaGuard ] | ||
64 | }, | 81 | }, |
65 | { | 82 | { |
66 | path: 'video-playlists/watch', | 83 | path: 'video-playlists/watch', |
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index e21ada0f1..0543564b4 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss | |||
@@ -40,8 +40,10 @@ | |||
40 | } | 40 | } |
41 | 41 | ||
42 | .icon-menu { | 42 | .icon-menu { |
43 | background-color: pvar(--mainForegroundColor); | ||
44 | mask-image: url('../assets/images/misc/menu.svg'); | 43 | mask-image: url('../assets/images/misc/menu.svg'); |
44 | -webkit-mask-image: url('../assets/images/misc/menu.svg'); | ||
45 | |||
46 | background-color: pvar(--mainForegroundColor); | ||
45 | margin: 0 18px 0 20px; | 47 | margin: 0 18px 0 20px; |
46 | 48 | ||
47 | @media screen and (max-width: $mobile-view) { | 49 | @media screen and (max-width: $mobile-view) { |
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 66d871b4a..863c3f3b5 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts | |||
@@ -67,7 +67,7 @@ export class AppComponent implements OnInit, AfterViewInit { | |||
67 | } | 67 | } |
68 | 68 | ||
69 | goToDefaultRoute () { | 69 | goToDefaultRoute () { |
70 | return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE) | 70 | return this.router.navigateByUrl(this.redirectService.getDefaultRoute()) |
71 | } | 71 | } |
72 | 72 | ||
73 | ngOnInit () { | 73 | ngOnInit () { |
@@ -231,7 +231,7 @@ export class AppComponent implements OnInit, AfterViewInit { | |||
231 | } | 231 | } |
232 | 232 | ||
233 | this.broadcastMessage = { | 233 | this.broadcastMessage = { |
234 | message: await this.markdownService.completeMarkdownToHTML(messageConfig.message), | 234 | message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true), |
235 | dismissable: messageConfig.dismissable, | 235 | dismissable: messageConfig.dismissable, |
236 | class: classes[messageConfig.level] | 236 | class: classes[messageConfig.level] |
237 | } | 237 | } |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 3cec6d739..9f46d49a2 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -4,9 +4,7 @@ import { APP_BASE_HREF, registerLocaleData } from '@angular/common' | |||
4 | import { NgModule } from '@angular/core' | 4 | import { NgModule } from '@angular/core' |
5 | import { BrowserModule } from '@angular/platform-browser' | 5 | import { BrowserModule } from '@angular/platform-browser' |
6 | import { ServiceWorkerModule } from '@angular/service-worker' | 6 | import { ServiceWorkerModule } from '@angular/service-worker' |
7 | import { ServerService } from '@app/core' | ||
8 | import localeOc from '@app/helpers/locales/oc' | 7 | import localeOc from '@app/helpers/locales/oc' |
9 | import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core' | ||
10 | import { AppRoutingModule } from './app-routing.module' | 8 | import { AppRoutingModule } from './app-routing.module' |
11 | import { AppComponent } from './app.component' | 9 | import { AppComponent } from './app.component' |
12 | import { CoreModule } from './core' | 10 | import { CoreModule } from './core' |
@@ -19,12 +17,12 @@ import { CustomModalComponent } from './modal/custom-modal.component' | |||
19 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' | 17 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' |
20 | import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' | 18 | import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' |
21 | import { WelcomeModalComponent } from './modal/welcome-modal.component' | 19 | import { WelcomeModalComponent } from './modal/welcome-modal.component' |
20 | import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' | ||
22 | import { SharedFormModule } from './shared/shared-forms' | 21 | import { SharedFormModule } from './shared/shared-forms' |
23 | import { SharedGlobalIconModule } from './shared/shared-icons' | 22 | import { SharedGlobalIconModule } from './shared/shared-icons' |
24 | import { SharedInstanceModule } from './shared/shared-instance' | 23 | import { SharedInstanceModule } from './shared/shared-instance' |
25 | import { SharedMainModule } from './shared/shared-main' | 24 | import { SharedMainModule } from './shared/shared-main' |
26 | import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings' | 25 | import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings' |
27 | import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' | ||
28 | 26 | ||
29 | registerLocaleData(localeOc, 'oc') | 27 | registerLocaleData(localeOc, 'oc') |
30 | 28 | ||
@@ -62,22 +60,6 @@ registerLocaleData(localeOc, 'oc') | |||
62 | SharedInstanceModule, | 60 | SharedInstanceModule, |
63 | SharedActorImageModule, | 61 | SharedActorImageModule, |
64 | 62 | ||
65 | MetaModule.forRoot({ | ||
66 | provide: MetaLoader, | ||
67 | useFactory: (serverService: ServerService) => { | ||
68 | return new MetaStaticLoader({ | ||
69 | pageTitlePositioning: PageTitlePositioning.PrependPageTitle, | ||
70 | pageTitleSeparator: ' - ', | ||
71 | get applicationName () { return serverService.getTmpConfig().instance.name }, | ||
72 | defaults: { | ||
73 | get title () { return serverService.getTmpConfig().instance.name }, | ||
74 | get description () { return serverService.getTmpConfig().instance.shortDescription } | ||
75 | } | ||
76 | }) | ||
77 | }, | ||
78 | deps: [ ServerService ] | ||
79 | }), | ||
80 | |||
81 | AppRoutingModule // Put it after all the module because it has the 404 route | 63 | AppRoutingModule // Put it after all the module because it has the 404 route |
82 | ], | 64 | ], |
83 | 65 | ||
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 3152a7003..de3274544 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -14,7 +14,7 @@ import { throwIfAlreadyLoaded } from './module-import-guard' | |||
14 | import { Notifier } from './notification' | 14 | import { Notifier } from './notification' |
15 | import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer' | 15 | import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer' |
16 | import { RestExtractor, RestService } from './rest' | 16 | import { RestExtractor, RestService } from './rest' |
17 | import { LoginGuard, RedirectService, UnloggedGuard, UserRightGuard } from './routing' | 17 | import { LoginGuard, MetaGuard, MetaService, RedirectService, UnloggedGuard, UserRightGuard } from './routing' |
18 | import { CanDeactivateGuard } from './routing/can-deactivate-guard.service' | 18 | import { CanDeactivateGuard } from './routing/can-deactivate-guard.service' |
19 | import { ServerConfigResolver } from './routing/server-config-resolver.service' | 19 | import { ServerConfigResolver } from './routing/server-config-resolver.service' |
20 | import { ScopedTokensService } from './scoped-tokens' | 20 | import { ScopedTokensService } from './scoped-tokens' |
@@ -77,7 +77,10 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra | |||
77 | MessageService, | 77 | MessageService, |
78 | PeerTubeSocket, | 78 | PeerTubeSocket, |
79 | ServerConfigResolver, | 79 | ServerConfigResolver, |
80 | CanDeactivateGuard | 80 | CanDeactivateGuard, |
81 | |||
82 | MetaService, | ||
83 | MetaGuard | ||
81 | ] | 84 | ] |
82 | }) | 85 | }) |
83 | export class CoreModule { | 86 | export class CoreModule { |
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts index 502d3bb2f..77592cbb6 100644 --- a/client/src/app/core/menu/menu.service.ts +++ b/client/src/app/core/menu/menu.service.ts | |||
@@ -1,8 +1,19 @@ | |||
1 | import { fromEvent } from 'rxjs' | 1 | import { fromEvent } from 'rxjs' |
2 | import { debounceTime } from 'rxjs/operators' | 2 | import { debounceTime } from 'rxjs/operators' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { GlobalIconName } from '@app/shared/shared-icons' | ||
5 | import { sortObjectComparator } from '@shared/core-utils/miscs/miscs' | ||
6 | import { ServerConfig } from '@shared/models/server' | ||
4 | import { ScreenService } from '../wrappers' | 7 | import { ScreenService } from '../wrappers' |
5 | 8 | ||
9 | export type MenuLink = { | ||
10 | icon: GlobalIconName | ||
11 | label: string | ||
12 | menuLabel: string | ||
13 | path: string | ||
14 | priority: number | ||
15 | } | ||
16 | |||
6 | @Injectable() | 17 | @Injectable() |
7 | export class MenuService { | 18 | export class MenuService { |
8 | isMenuDisplayed = true | 19 | isMenuDisplayed = true |
@@ -48,6 +59,53 @@ export class MenuService { | |||
48 | this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser | 59 | this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser |
49 | } | 60 | } |
50 | 61 | ||
62 | buildCommonLinks (config: ServerConfig) { | ||
63 | let entries: MenuLink[] = [ | ||
64 | { | ||
65 | icon: 'globe' as 'globe', | ||
66 | label: $localize`Discover videos`, | ||
67 | menuLabel: $localize`Discover`, | ||
68 | path: '/videos/overview', | ||
69 | priority: 150 | ||
70 | }, | ||
71 | { | ||
72 | icon: 'trending' as 'trending', | ||
73 | label: $localize`Trending videos`, | ||
74 | menuLabel: $localize`Trending`, | ||
75 | path: '/videos/trending', | ||
76 | priority: 140 | ||
77 | }, | ||
78 | { | ||
79 | icon: 'recently-added' as 'recently-added', | ||
80 | label: $localize`Recently added videos`, | ||
81 | menuLabel: $localize`Recently added`, | ||
82 | path: '/videos/recently-added', | ||
83 | priority: 130 | ||
84 | }, | ||
85 | { | ||
86 | icon: 'octagon' as 'octagon', | ||
87 | label: $localize`Local videos`, | ||
88 | menuLabel: $localize`Local videos`, | ||
89 | path: '/videos/local', | ||
90 | priority: 120 | ||
91 | } | ||
92 | ] | ||
93 | |||
94 | if (config.homepage.enabled) { | ||
95 | entries.push({ | ||
96 | icon: 'home' as 'home', | ||
97 | label: $localize`Home`, | ||
98 | menuLabel: $localize`Home`, | ||
99 | path: '/home', | ||
100 | priority: 160 | ||
101 | }) | ||
102 | } | ||
103 | |||
104 | entries = entries.sort(sortObjectComparator('priority', 'desc')) | ||
105 | |||
106 | return entries | ||
107 | } | ||
108 | |||
51 | private handleWindowResize () { | 109 | private handleWindowResize () { |
52 | // On touch screens, do not handle window resize event since opened menu is handled with a content overlay | 110 | // On touch screens, do not handle window resize event since opened menu is handled with a content overlay |
53 | if (this.screenService.isInTouchScreen()) return | 111 | if (this.screenService.isInTouchScreen()) return |
diff --git a/client/src/app/core/renderer/html-renderer.service.ts b/client/src/app/core/renderer/html-renderer.service.ts index 3176cf6a4..418d8603e 100644 --- a/client/src/app/core/renderer/html-renderer.service.ts +++ b/client/src/app/core/renderer/html-renderer.service.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { LinkifierService } from './linkifier.service' | 2 | import { LinkifierService } from './linkifier.service' |
3 | import { SANITIZE_OPTIONS } from '@shared/core-utils/renderer/html' | 3 | import { getCustomMarkupSanitizeOptions, getSanitizeOptions } from '@shared/core-utils/renderer/html' |
4 | 4 | ||
5 | @Injectable() | 5 | @Injectable() |
6 | export class HtmlRendererService { | 6 | export class HtmlRendererService { |
@@ -20,7 +20,7 @@ export class HtmlRendererService { | |||
20 | }) | 20 | }) |
21 | } | 21 | } |
22 | 22 | ||
23 | async toSafeHtml (text: string) { | 23 | async toSafeHtml (text: string, additionalAllowedTags: string[] = []) { |
24 | const [ html ] = await Promise.all([ | 24 | const [ html ] = await Promise.all([ |
25 | // Convert possible markdown to html | 25 | // Convert possible markdown to html |
26 | this.linkifier.linkify(text), | 26 | this.linkifier.linkify(text), |
@@ -28,7 +28,11 @@ export class HtmlRendererService { | |||
28 | this.loadSanitizeHtml() | 28 | this.loadSanitizeHtml() |
29 | ]) | 29 | ]) |
30 | 30 | ||
31 | return this.sanitizeHtml(html, SANITIZE_OPTIONS) | 31 | const options = additionalAllowedTags.length !== 0 |
32 | ? getCustomMarkupSanitizeOptions(additionalAllowedTags) | ||
33 | : getSanitizeOptions() | ||
34 | |||
35 | return this.sanitizeHtml(html, options) | ||
32 | } | 36 | } |
33 | 37 | ||
34 | private async loadSanitizeHtml () { | 38 | private async loadSanitizeHtml () { |
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts index edddb0a66..ca1bf4eb9 100644 --- a/client/src/app/core/renderer/markdown.service.ts +++ b/client/src/app/core/renderer/markdown.service.ts | |||
@@ -17,12 +17,15 @@ type MarkdownParsers = { | |||
17 | enhancedMarkdownIt: MarkdownIt | 17 | enhancedMarkdownIt: MarkdownIt |
18 | enhancedWithHTMLMarkdownIt: MarkdownIt | 18 | enhancedWithHTMLMarkdownIt: MarkdownIt |
19 | 19 | ||
20 | completeMarkdownIt: MarkdownIt | 20 | unsafeMarkdownIt: MarkdownIt |
21 | |||
22 | customPageMarkdownIt: MarkdownIt | ||
21 | } | 23 | } |
22 | 24 | ||
23 | type MarkdownConfig = { | 25 | type MarkdownConfig = { |
24 | rules: string[] | 26 | rules: string[] |
25 | html: boolean | 27 | html: boolean |
28 | breaks: boolean | ||
26 | escape?: boolean | 29 | escape?: boolean |
27 | } | 30 | } |
28 | 31 | ||
@@ -35,18 +38,24 @@ export class MarkdownService { | |||
35 | private markdownParsers: MarkdownParsers = { | 38 | private markdownParsers: MarkdownParsers = { |
36 | textMarkdownIt: null, | 39 | textMarkdownIt: null, |
37 | textWithHTMLMarkdownIt: null, | 40 | textWithHTMLMarkdownIt: null, |
41 | |||
38 | enhancedMarkdownIt: null, | 42 | enhancedMarkdownIt: null, |
39 | enhancedWithHTMLMarkdownIt: null, | 43 | enhancedWithHTMLMarkdownIt: null, |
40 | completeMarkdownIt: null | 44 | |
45 | unsafeMarkdownIt: null, | ||
46 | |||
47 | customPageMarkdownIt: null | ||
41 | } | 48 | } |
42 | private parsersConfig: MarkdownParserConfigs = { | 49 | private parsersConfig: MarkdownParserConfigs = { |
43 | textMarkdownIt: { rules: TEXT_RULES, html: false }, | 50 | textMarkdownIt: { rules: TEXT_RULES, breaks: true, html: false }, |
44 | textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, html: true, escape: true }, | 51 | textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, breaks: true, html: true, escape: true }, |
45 | 52 | ||
46 | enhancedMarkdownIt: { rules: ENHANCED_RULES, html: false }, | 53 | enhancedMarkdownIt: { rules: ENHANCED_RULES, breaks: true, html: false }, |
47 | enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, html: true, escape: true }, | 54 | enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, breaks: true, html: true, escape: true }, |
48 | 55 | ||
49 | completeMarkdownIt: { rules: COMPLETE_RULES, html: true } | 56 | unsafeMarkdownIt: { rules: COMPLETE_RULES, breaks: true, html: true, escape: false }, |
57 | |||
58 | customPageMarkdownIt: { rules: COMPLETE_RULES, breaks: false, html: true, escape: true } | ||
50 | } | 59 | } |
51 | 60 | ||
52 | private emojiModule: any | 61 | private emojiModule: any |
@@ -54,22 +63,26 @@ export class MarkdownService { | |||
54 | constructor (private htmlRenderer: HtmlRendererService) {} | 63 | constructor (private htmlRenderer: HtmlRendererService) {} |
55 | 64 | ||
56 | textMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) { | 65 | textMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) { |
57 | if (withHtml) return this.render('textWithHTMLMarkdownIt', markdown, withEmoji) | 66 | if (withHtml) return this.render({ name: 'textWithHTMLMarkdownIt', markdown, withEmoji }) |
58 | 67 | ||
59 | return this.render('textMarkdownIt', markdown, withEmoji) | 68 | return this.render({ name: 'textMarkdownIt', markdown, withEmoji }) |
60 | } | 69 | } |
61 | 70 | ||
62 | enhancedMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) { | 71 | enhancedMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) { |
63 | if (withHtml) return this.render('enhancedWithHTMLMarkdownIt', markdown, withEmoji) | 72 | if (withHtml) return this.render({ name: 'enhancedWithHTMLMarkdownIt', markdown, withEmoji }) |
73 | |||
74 | return this.render({ name: 'enhancedMarkdownIt', markdown, withEmoji }) | ||
75 | } | ||
64 | 76 | ||
65 | return this.render('enhancedMarkdownIt', markdown, withEmoji) | 77 | unsafeMarkdownToHTML (markdown: string, _trustedInput: true) { |
78 | return this.render({ name: 'unsafeMarkdownIt', markdown, withEmoji: true }) | ||
66 | } | 79 | } |
67 | 80 | ||
68 | completeMarkdownToHTML (markdown: string) { | 81 | customPageMarkdownToHTML (markdown: string, additionalAllowedTags: string[]) { |
69 | return this.render('completeMarkdownIt', markdown, true) | 82 | return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags }) |
70 | } | 83 | } |
71 | 84 | ||
72 | async processVideoTimestamps (html: string) { | 85 | processVideoTimestamps (html: string) { |
73 | return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { | 86 | return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { |
74 | const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) | 87 | const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) |
75 | const url = buildVideoLink({ startTime: t }) | 88 | const url = buildVideoLink({ startTime: t }) |
@@ -77,7 +90,13 @@ export class MarkdownService { | |||
77 | }) | 90 | }) |
78 | } | 91 | } |
79 | 92 | ||
80 | private async render (name: keyof MarkdownParsers, markdown: string, withEmoji = false) { | 93 | private async render (options: { |
94 | name: keyof MarkdownParsers | ||
95 | markdown: string | ||
96 | withEmoji: boolean | ||
97 | additionalAllowedTags?: string[] | ||
98 | }) { | ||
99 | const { name, markdown, withEmoji, additionalAllowedTags } = options | ||
81 | if (!markdown) return '' | 100 | if (!markdown) return '' |
82 | 101 | ||
83 | const config = this.parsersConfig[ name ] | 102 | const config = this.parsersConfig[ name ] |
@@ -96,7 +115,7 @@ export class MarkdownService { | |||
96 | let html = this.markdownParsers[ name ].render(markdown) | 115 | let html = this.markdownParsers[ name ].render(markdown) |
97 | html = this.avoidTruncatedTags(html) | 116 | html = this.avoidTruncatedTags(html) |
98 | 117 | ||
99 | if (config.escape) return this.htmlRenderer.toSafeHtml(html) | 118 | if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags) |
100 | 119 | ||
101 | return html | 120 | return html |
102 | } | 121 | } |
@@ -105,7 +124,7 @@ export class MarkdownService { | |||
105 | // FIXME: import('...') returns a struct module, containing a "default" field | 124 | // FIXME: import('...') returns a struct module, containing a "default" field |
106 | const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default | 125 | const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default |
107 | 126 | ||
108 | const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html }) | 127 | const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: config.breaks, html: config.html }) |
109 | 128 | ||
110 | for (const rule of config.rules) { | 129 | for (const rule of config.rules) { |
111 | markdownIt.enable(rule) | 130 | markdownIt.enable(rule) |
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts index 239c27caf..4314ea475 100644 --- a/client/src/app/core/routing/index.ts +++ b/client/src/app/core/routing/index.ts | |||
@@ -3,6 +3,8 @@ export * from './custom-reuse-strategy' | |||
3 | export * from './disable-for-reuse-hook' | 3 | export * from './disable-for-reuse-hook' |
4 | export * from './login-guard.service' | 4 | export * from './login-guard.service' |
5 | export * from './menu-guard.service' | 5 | export * from './menu-guard.service' |
6 | export * from './meta-guard.service' | ||
7 | export * from './meta.service' | ||
6 | export * from './preload-selected-modules-list' | 8 | export * from './preload-selected-modules-list' |
7 | export * from './redirect.service' | 9 | export * from './redirect.service' |
8 | export * from './server-config-resolver.service' | 10 | export * from './server-config-resolver.service' |
diff --git a/client/src/app/core/routing/meta-guard.service.ts b/client/src/app/core/routing/meta-guard.service.ts new file mode 100644 index 000000000..bedb3450e --- /dev/null +++ b/client/src/app/core/routing/meta-guard.service.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router' | ||
3 | import { MetaService } from './meta.service' | ||
4 | |||
5 | @Injectable() | ||
6 | export class MetaGuard implements CanActivate, CanActivateChild { | ||
7 | |||
8 | constructor (private meta: MetaService) { } | ||
9 | |||
10 | canActivate (route: ActivatedRouteSnapshot): boolean { | ||
11 | const metaSettings = route.data?.meta | ||
12 | |||
13 | if (metaSettings) { | ||
14 | this.meta.update(metaSettings) | ||
15 | } | ||
16 | |||
17 | return true | ||
18 | } | ||
19 | |||
20 | canActivateChild (route: ActivatedRouteSnapshot): boolean { | ||
21 | return this.canActivate(route) | ||
22 | } | ||
23 | } | ||
diff --git a/client/src/app/core/routing/meta.service.ts b/client/src/app/core/routing/meta.service.ts new file mode 100644 index 000000000..a5ac778dc --- /dev/null +++ b/client/src/app/core/routing/meta.service.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { Meta, Title } from '@angular/platform-browser' | ||
3 | import { HTMLServerConfig } from '@shared/models/server' | ||
4 | import { ServerService } from '../server' | ||
5 | |||
6 | export interface MetaSettings { | ||
7 | title?: string | ||
8 | } | ||
9 | |||
10 | @Injectable() | ||
11 | export class MetaService { | ||
12 | private config: HTMLServerConfig | ||
13 | |||
14 | constructor ( | ||
15 | private titleService: Title, | ||
16 | private meta: Meta, | ||
17 | private server: ServerService | ||
18 | ) { | ||
19 | this.config = this.server.getTmpConfig() | ||
20 | this.server.getConfig() | ||
21 | .subscribe(config => this.config = config) | ||
22 | } | ||
23 | |||
24 | setTitle (subTitle?: string) { | ||
25 | let title = '' | ||
26 | if (subTitle) title += `${subTitle} - ` | ||
27 | |||
28 | title += this.config.instance.name | ||
29 | |||
30 | this.titleService.setTitle(title) | ||
31 | } | ||
32 | |||
33 | setTag (name: string, value: string) { | ||
34 | this.meta.addTag({ name, content: value }) | ||
35 | } | ||
36 | |||
37 | update (meta: MetaSettings) { | ||
38 | this.setTitle(meta.title) | ||
39 | } | ||
40 | } | ||
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts index 6d26fb504..cf690a4d0 100644 --- a/client/src/app/core/routing/redirect.service.ts +++ b/client/src/app/core/routing/redirect.service.ts | |||
@@ -6,14 +6,14 @@ import { ServerService } from '../server' | |||
6 | export class RedirectService { | 6 | export class RedirectService { |
7 | // Default route could change according to the instance configuration | 7 | // Default route could change according to the instance configuration |
8 | static INIT_DEFAULT_ROUTE = '/videos/trending' | 8 | static INIT_DEFAULT_ROUTE = '/videos/trending' |
9 | static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE | ||
10 | static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed' | 9 | static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed' |
11 | static DEFAULT_TRENDING_ALGORITHM = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM | ||
12 | 10 | ||
13 | private previousUrl: string | 11 | private previousUrl: string |
14 | private currentUrl: string | 12 | private currentUrl: string |
15 | 13 | ||
16 | private redirectingToHomepage = false | 14 | private redirectingToHomepage = false |
15 | private defaultTrendingAlgorithm = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM | ||
16 | private defaultRoute = RedirectService.INIT_DEFAULT_ROUTE | ||
17 | 17 | ||
18 | constructor ( | 18 | constructor ( |
19 | private router: Router, | 19 | private router: Router, |
@@ -22,10 +22,10 @@ export class RedirectService { | |||
22 | // The config is first loaded from the cache so try to get the default route | 22 | // The config is first loaded from the cache so try to get the default route |
23 | const tmpConfig = this.serverService.getTmpConfig() | 23 | const tmpConfig = this.serverService.getTmpConfig() |
24 | if (tmpConfig?.instance?.defaultClientRoute) { | 24 | if (tmpConfig?.instance?.defaultClientRoute) { |
25 | RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute | 25 | this.defaultRoute = tmpConfig.instance.defaultClientRoute |
26 | } | 26 | } |
27 | if (tmpConfig?.trending?.videos?.algorithms?.default) { | 27 | if (tmpConfig?.trending?.videos?.algorithms?.default) { |
28 | RedirectService.DEFAULT_TRENDING_ALGORITHM = tmpConfig.trending.videos.algorithms.default | 28 | this.defaultTrendingAlgorithm = tmpConfig.trending.videos.algorithms.default |
29 | } | 29 | } |
30 | 30 | ||
31 | // Load default route | 31 | // Load default route |
@@ -34,13 +34,8 @@ export class RedirectService { | |||
34 | const defaultRouteConfig = config.instance.defaultClientRoute | 34 | const defaultRouteConfig = config.instance.defaultClientRoute |
35 | const defaultTrendingConfig = config.trending.videos.algorithms.default | 35 | const defaultTrendingConfig = config.trending.videos.algorithms.default |
36 | 36 | ||
37 | if (defaultRouteConfig) { | 37 | if (defaultRouteConfig) this.defaultRoute = defaultRouteConfig |
38 | RedirectService.DEFAULT_ROUTE = defaultRouteConfig | 38 | if (defaultTrendingConfig) this.defaultTrendingAlgorithm = defaultTrendingConfig |
39 | } | ||
40 | |||
41 | if (defaultTrendingConfig) { | ||
42 | RedirectService.DEFAULT_TRENDING_ALGORITHM = defaultTrendingConfig | ||
43 | } | ||
44 | }) | 39 | }) |
45 | 40 | ||
46 | // Track previous url | 41 | // Track previous url |
@@ -53,6 +48,14 @@ export class RedirectService { | |||
53 | }) | 48 | }) |
54 | } | 49 | } |
55 | 50 | ||
51 | getDefaultRoute () { | ||
52 | return this.defaultRoute | ||
53 | } | ||
54 | |||
55 | getDefaultTrendingAlgorithm () { | ||
56 | return this.defaultTrendingAlgorithm | ||
57 | } | ||
58 | |||
56 | redirectToPreviousRoute () { | 59 | redirectToPreviousRoute () { |
57 | const exceptions = [ | 60 | const exceptions = [ |
58 | '/verify-account', | 61 | '/verify-account', |
@@ -72,21 +75,21 @@ export class RedirectService { | |||
72 | 75 | ||
73 | this.redirectingToHomepage = true | 76 | this.redirectingToHomepage = true |
74 | 77 | ||
75 | console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE) | 78 | console.log('Redirecting to %s...', this.defaultRoute) |
76 | 79 | ||
77 | this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange }) | 80 | this.router.navigateByUrl(this.defaultRoute, { skipLocationChange }) |
78 | .then(() => this.redirectingToHomepage = false) | 81 | .then(() => this.redirectingToHomepage = false) |
79 | .catch(() => { | 82 | .catch(() => { |
80 | this.redirectingToHomepage = false | 83 | this.redirectingToHomepage = false |
81 | 84 | ||
82 | console.error( | 85 | console.error( |
83 | 'Cannot navigate to %s, resetting default route to %s.', | 86 | 'Cannot navigate to %s, resetting default route to %s.', |
84 | RedirectService.DEFAULT_ROUTE, | 87 | this.defaultRoute, |
85 | RedirectService.INIT_DEFAULT_ROUTE | 88 | RedirectService.INIT_DEFAULT_ROUTE |
86 | ) | 89 | ) |
87 | 90 | ||
88 | RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE | 91 | this.defaultRoute = RedirectService.INIT_DEFAULT_ROUTE |
89 | return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange }) | 92 | return this.router.navigateByUrl(this.defaultRoute, { skipLocationChange }) |
90 | }) | 93 | }) |
91 | 94 | ||
92 | } | 95 | } |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 906191ae1..5b1b7603f 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -3,7 +3,6 @@ import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators' | |||
3 | import { HttpClient } from '@angular/common/http' | 3 | import { HttpClient } from '@angular/common/http' |
4 | import { Inject, Injectable, LOCALE_ID } from '@angular/core' | 4 | import { Inject, Injectable, LOCALE_ID } from '@angular/core' |
5 | import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers' | 5 | import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers' |
6 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' | ||
7 | import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' | 6 | import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' |
8 | import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models' | 7 | import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models' |
9 | import { environment } from '../../../environments/environment' | 8 | import { environment } from '../../../environments/environment' |
@@ -16,8 +15,6 @@ export class ServerService { | |||
16 | private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' | 15 | private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' |
17 | private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats' | 16 | private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats' |
18 | 17 | ||
19 | private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' | ||
20 | |||
21 | configReloaded = new Subject<ServerConfig>() | 18 | configReloaded = new Subject<ServerConfig>() |
22 | 19 | ||
23 | private localeObservable: Observable<any> | 20 | private localeObservable: Observable<any> |
@@ -176,6 +173,9 @@ export class ServerService { | |||
176 | disableLocalSearch: false, | 173 | disableLocalSearch: false, |
177 | isDefaultSearch: false | 174 | isDefaultSearch: false |
178 | } | 175 | } |
176 | }, | ||
177 | homepage: { | ||
178 | enabled: false | ||
179 | } | 179 | } |
180 | } | 180 | } |
181 | 181 | ||
@@ -201,9 +201,7 @@ export class ServerService { | |||
201 | this.configReset = true | 201 | this.configReset = true |
202 | 202 | ||
203 | // Notify config update | 203 | // Notify config update |
204 | this.getConfig().subscribe(() => { | 204 | return this.getConfig() |
205 | // empty, to fire a reset config event | ||
206 | }) | ||
207 | } | 205 | } |
208 | 206 | ||
209 | getConfig () { | 207 | getConfig () { |
@@ -212,7 +210,6 @@ export class ServerService { | |||
212 | if (!this.configObservable) { | 210 | if (!this.configObservable) { |
213 | this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL) | 211 | this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL) |
214 | .pipe( | 212 | .pipe( |
215 | tap(config => this.saveConfigLocally(config)), | ||
216 | tap(config => { | 213 | tap(config => { |
217 | this.config = config | 214 | this.config = config |
218 | this.configLoaded = true | 215 | this.configLoaded = true |
@@ -343,20 +340,15 @@ export class ServerService { | |||
343 | ) | 340 | ) |
344 | } | 341 | } |
345 | 342 | ||
346 | private saveConfigLocally (config: ServerConfig) { | ||
347 | peertubeLocalStorage.setItem(ServerService.CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)) | ||
348 | } | ||
349 | |||
350 | private loadConfigLocally () { | 343 | private loadConfigLocally () { |
351 | const configString = peertubeLocalStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY) | 344 | const configString = window['PeerTubeServerConfig'] |
352 | 345 | if (!configString) return | |
353 | if (configString) { | 346 | |
354 | try { | 347 | try { |
355 | const parsed = JSON.parse(configString) | 348 | const parsed = JSON.parse(configString) |
356 | Object.assign(this.config, parsed) | 349 | Object.assign(this.config, parsed) |
357 | } catch (err) { | 350 | } catch (err) { |
358 | console.error('Cannot parse config saved in local storage.', err) | 351 | console.error('Cannot parse config saved in from index.html.', err) |
359 | } | ||
360 | } | 352 | } |
361 | } | 353 | } |
362 | } | 354 | } |
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts index 4c4611d01..e7a5ae17a 100644 --- a/client/src/app/core/theme/theme.service.ts +++ b/client/src/app/core/theme/theme.service.ts | |||
@@ -82,7 +82,19 @@ export class ThemeService { | |||
82 | : this.userService.getAnonymousUser().theme | 82 | : this.userService.getAnonymousUser().theme |
83 | 83 | ||
84 | if (theme !== 'instance-default') return theme | 84 | if (theme !== 'instance-default') return theme |
85 | return this.serverConfig.theme.default | 85 | |
86 | const instanceTheme = this.serverConfig.theme.default | ||
87 | if (instanceTheme !== 'default') return instanceTheme | ||
88 | |||
89 | // Default to dark theme if available and wanted by the user | ||
90 | if ( | ||
91 | this.themes.find(t => t.name === 'dark') && | ||
92 | window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches | ||
93 | ) { | ||
94 | return 'dark' | ||
95 | } | ||
96 | |||
97 | return instanceTheme | ||
86 | } | 98 | } |
87 | 99 | ||
88 | private loadTheme (name: string) { | 100 | private loadTheme (name: string) { |
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts index a1747af3c..94f6def26 100644 --- a/client/src/app/helpers/utils.ts +++ b/client/src/app/helpers/utils.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { map } from 'rxjs/operators' | 1 | import { first, map } from 'rxjs/operators' |
2 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 2 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
3 | import { DatePipe } from '@angular/common' | 3 | import { DatePipe } from '@angular/common' |
4 | import { HttpErrorResponse } from '@angular/common/http' | 4 | import { HttpErrorResponse } from '@angular/common/http' |
@@ -23,20 +23,29 @@ function getParameterByName (name: string, url: string) { | |||
23 | 23 | ||
24 | function listUserChannels (authService: AuthService) { | 24 | function listUserChannels (authService: AuthService) { |
25 | return authService.userInformationLoaded | 25 | return authService.userInformationLoaded |
26 | .pipe(map(() => { | 26 | .pipe( |
27 | const user = authService.getUser() | 27 | first(), |
28 | if (!user) return undefined | 28 | map(() => { |
29 | 29 | const user = authService.getUser() | |
30 | const videoChannels = user.videoChannels | 30 | if (!user) return undefined |
31 | if (Array.isArray(videoChannels) === false) return undefined | 31 | |
32 | 32 | const videoChannels = user.videoChannels | |
33 | return videoChannels.map(c => ({ | 33 | if (Array.isArray(videoChannels) === false) return undefined |
34 | id: c.id, | 34 | |
35 | label: c.displayName, | 35 | return videoChannels |
36 | support: c.support, | 36 | .sort((a, b) => { |
37 | avatarPath: c.avatar?.path | 37 | if (a.updatedAt < b.updatedAt) return 1 |
38 | }) as SelectChannelItem) | 38 | if (a.updatedAt > b.updatedAt) return -1 |
39 | })) | 39 | return 0 |
40 | }) | ||
41 | .map(c => ({ | ||
42 | id: c.id, | ||
43 | label: c.displayName, | ||
44 | support: c.support, | ||
45 | avatarPath: c.avatar?.path | ||
46 | }) as SelectChannelItem) | ||
47 | }) | ||
48 | ) | ||
40 | } | 49 | } |
41 | 50 | ||
42 | function getAbsoluteAPIUrl () { | 51 | function getAbsoluteAPIUrl () { |
@@ -167,8 +176,8 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) { | |||
167 | ) | 176 | ) |
168 | } | 177 | } |
169 | 178 | ||
170 | function uploadErrorHandler (parameters: { | 179 | function genericUploadErrorHandler (parameters: { |
171 | err: HttpErrorResponse | 180 | err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'> |
172 | name: string | 181 | name: string |
173 | notifier: Notifier | 182 | notifier: Notifier |
174 | sticky?: boolean | 183 | sticky?: boolean |
@@ -180,6 +189,9 @@ function uploadErrorHandler (parameters: { | |||
180 | if (err instanceof ErrorEvent) { // network error | 189 | if (err instanceof ErrorEvent) { // network error |
181 | message = $localize`The connection was interrupted` | 190 | message = $localize`The connection was interrupted` |
182 | notifier.error(message, title, null, sticky) | 191 | notifier.error(message, title, null, sticky) |
192 | } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) { | ||
193 | message = $localize`The server encountered an error` | ||
194 | notifier.error(message, title, null, sticky) | ||
183 | } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { | 195 | } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { |
184 | message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` | 196 | message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` |
185 | notifier.error(message, title, null, sticky) | 197 | notifier.error(message, title, null, sticky) |
@@ -210,5 +222,5 @@ export { | |||
210 | isInViewport, | 222 | isInViewport, |
211 | isXPercentInViewport, | 223 | isXPercentInViewport, |
212 | listUserChannels, | 224 | listUserChannels, |
213 | uploadErrorHandler | 225 | genericUploadErrorHandler |
214 | } | 226 | } |
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 3a7ffcbb6..2c2c4f260 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -123,24 +123,9 @@ | |||
123 | <div class="on-instance"> | 123 | <div class="on-instance"> |
124 | <div i18n class="block-title">ON {{instanceName}}</div> | 124 | <div i18n class="block-title">ON {{instanceName}}</div> |
125 | 125 | ||
126 | <a class="menu-link" routerLink="/videos/overview" routerLinkActive="active"> | 126 | <a class="menu-link" *ngFor="let commonLink of commonMenuLinks" [routerLink]="commonLink.path" routerLinkActive="active"> |
127 | <my-global-icon iconName="globe" aria-hidden="true"></my-global-icon> | 127 | <my-global-icon [iconName]="commonLink.icon" aria-hidden="true"></my-global-icon> |
128 | <ng-container i18n>Discover</ng-container> | 128 | <ng-container>{{ commonLink.menuLabel }}</ng-container> |
129 | </a> | ||
130 | |||
131 | <a class="menu-link" routerLink="/videos/trending" routerLinkActive="active"> | ||
132 | <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon> | ||
133 | <ng-container i18n>Trending</ng-container> | ||
134 | </a> | ||
135 | |||
136 | <a class="menu-link" routerLink="/videos/recently-added" routerLinkActive="active"> | ||
137 | <my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon> | ||
138 | <ng-container i18n>Recently added</ng-container> | ||
139 | </a> | ||
140 | |||
141 | <a class="menu-link" routerLink="/videos/local" routerLinkActive="active"> | ||
142 | <my-global-icon iconName="home" aria-hidden="true"></my-global-icon> | ||
143 | <ng-container i18n>Local videos</ng-container> | ||
144 | </a> | 129 | </a> |
145 | </div> | 130 | </div> |
146 | </div> | 131 | </div> |
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 8fa1de326..2f7e0cf07 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -4,7 +4,17 @@ import { switchMap } from 'rxjs/operators' | |||
4 | import { ViewportScroller } from '@angular/common' | 4 | import { ViewportScroller } from '@angular/common' |
5 | import { Component, OnInit, ViewChild } from '@angular/core' | 5 | import { Component, OnInit, ViewChild } from '@angular/core' |
6 | import { Router } from '@angular/router' | 6 | import { Router } from '@angular/router' |
7 | import { AuthService, AuthStatus, AuthUser, MenuService, RedirectService, ScreenService, ServerService, UserService } from '@app/core' | 7 | import { |
8 | AuthService, | ||
9 | AuthStatus, | ||
10 | AuthUser, | ||
11 | MenuLink, | ||
12 | MenuService, | ||
13 | RedirectService, | ||
14 | ScreenService, | ||
15 | ServerService, | ||
16 | UserService | ||
17 | } from '@app/core' | ||
8 | import { scrollToTop } from '@app/helpers' | 18 | import { scrollToTop } from '@app/helpers' |
9 | import { LanguageChooserComponent } from '@app/menu/language-chooser.component' | 19 | import { LanguageChooserComponent } from '@app/menu/language-chooser.component' |
10 | import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' | 20 | import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' |
@@ -35,6 +45,8 @@ export class MenuComponent implements OnInit { | |||
35 | 45 | ||
36 | currentInterfaceLanguage: string | 46 | currentInterfaceLanguage: string |
37 | 47 | ||
48 | commonMenuLinks: MenuLink[] = [] | ||
49 | |||
38 | private languages: VideoConstant<string>[] = [] | 50 | private languages: VideoConstant<string>[] = [] |
39 | private serverConfig: ServerConfig | 51 | private serverConfig: ServerConfig |
40 | private routesPerRight: { [role in UserRight]?: string } = { | 52 | private routesPerRight: { [role in UserRight]?: string } = { |
@@ -80,7 +92,10 @@ export class MenuComponent implements OnInit { | |||
80 | ngOnInit () { | 92 | ngOnInit () { |
81 | this.serverConfig = this.serverService.getTmpConfig() | 93 | this.serverConfig = this.serverService.getTmpConfig() |
82 | this.serverService.getConfig() | 94 | this.serverService.getConfig() |
83 | .subscribe(config => this.serverConfig = config) | 95 | .subscribe(config => { |
96 | this.serverConfig = config | ||
97 | this.buildMenuLinks() | ||
98 | }) | ||
84 | 99 | ||
85 | this.isLoggedIn = this.authService.isLoggedIn() | 100 | this.isLoggedIn = this.authService.isLoggedIn() |
86 | if (this.isLoggedIn === true) { | 101 | if (this.isLoggedIn === true) { |
@@ -241,6 +256,10 @@ export class MenuComponent implements OnInit { | |||
241 | } | 256 | } |
242 | } | 257 | } |
243 | 258 | ||
259 | private buildMenuLinks () { | ||
260 | this.commonMenuLinks = this.menuService.buildCommonLinks(this.serverConfig) | ||
261 | } | ||
262 | |||
244 | private buildUserLanguages () { | 263 | private buildUserLanguages () { |
245 | if (!this.user) { | 264 | if (!this.user) { |
246 | this.videoLanguages = [] | 265 | this.videoLanguages = [] |
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts b/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts index 8c12d3c4c..08372d8ad 100644 --- a/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts +++ b/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts | |||
@@ -42,7 +42,7 @@ export class ActorBannerEditComponent implements OnInit { | |||
42 | this.bannerExtensions = config.banner.file.extensions.join(', ') | 42 | this.bannerExtensions = config.banner.file.extensions.join(', ') |
43 | 43 | ||
44 | // tslint:disable:max-line-length | 44 | // tslint:disable:max-line-length |
45 | this.bannerFormat = $localize`ratio 6/1, recommended size: 1600x266, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}` | 45 | this.bannerFormat = $localize`ratio 6/1, recommended size: 1920x317, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}` |
46 | }) | 46 | }) |
47 | } | 47 | } |
48 | 48 | ||
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html new file mode 100644 index 000000000..da81006b9 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html | |||
@@ -0,0 +1,8 @@ | |||
1 | <div *ngIf="channel" class="channel"> | ||
2 | <my-actor-avatar [channel]="channel" size="34"></my-actor-avatar> | ||
3 | |||
4 | <div class="display-name">{{ channel.displayName }}</div> | ||
5 | <div class="username">{{ channel.name }}</div> | ||
6 | |||
7 | <div class="description">{{ channel.description }}</div> | ||
8 | </div> | ||
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss new file mode 100644 index 000000000..85018afe2 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss | |||
@@ -0,0 +1,9 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .channel { | ||
5 | border-radius: 15px; | ||
6 | padding: 10px; | ||
7 | width: min-content; | ||
8 | border: 1px solid pvar(--mainColor); | ||
9 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts new file mode 100644 index 000000000..97bb5567e --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { VideoChannel, VideoChannelService } from '../shared-main' | ||
3 | |||
4 | /* | ||
5 | * Markup component that creates a channel miniature only | ||
6 | */ | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-channel-miniature-markup', | ||
10 | templateUrl: 'channel-miniature-markup.component.html', | ||
11 | styleUrls: [ 'channel-miniature-markup.component.scss' ] | ||
12 | }) | ||
13 | export class ChannelMiniatureMarkupComponent implements OnInit { | ||
14 | @Input() name: string | ||
15 | |||
16 | channel: VideoChannel | ||
17 | |||
18 | constructor ( | ||
19 | private channelService: VideoChannelService | ||
20 | ) { } | ||
21 | |||
22 | ngOnInit () { | ||
23 | this.channelService.getVideoChannel(this.name) | ||
24 | .subscribe(channel => this.channel = channel) | ||
25 | } | ||
26 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts new file mode 100644 index 000000000..ffaf15710 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts | |||
@@ -0,0 +1,136 @@ | |||
1 | import { ComponentRef, Injectable } from '@angular/core' | ||
2 | import { MarkdownService } from '@app/core' | ||
3 | import { | ||
4 | ChannelMiniatureMarkupData, | ||
5 | EmbedMarkupData, | ||
6 | PlaylistMiniatureMarkupData, | ||
7 | VideoMiniatureMarkupData, | ||
8 | VideosListMarkupData | ||
9 | } from '@shared/models' | ||
10 | import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component' | ||
11 | import { DynamicElementService } from './dynamic-element.service' | ||
12 | import { EmbedMarkupComponent } from './embed-markup.component' | ||
13 | import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component' | ||
14 | import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component' | ||
15 | import { VideosListMarkupComponent } from './videos-list-markup.component' | ||
16 | |||
17 | type BuilderFunction = (el: HTMLElement) => ComponentRef<any> | ||
18 | |||
19 | @Injectable() | ||
20 | export class CustomMarkupService { | ||
21 | private builders: { [ selector: string ]: BuilderFunction } = { | ||
22 | 'peertube-video-embed': el => this.embedBuilder(el, 'video'), | ||
23 | 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'), | ||
24 | 'peertube-video-miniature': el => this.videoMiniatureBuilder(el), | ||
25 | 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el), | ||
26 | 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el), | ||
27 | 'peertube-videos-list': el => this.videosListBuilder(el) | ||
28 | } | ||
29 | |||
30 | constructor ( | ||
31 | private dynamicElementService: DynamicElementService, | ||
32 | private markdown: MarkdownService | ||
33 | ) { } | ||
34 | |||
35 | async buildElement (text: string) { | ||
36 | const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags()) | ||
37 | |||
38 | const rootElement = document.createElement('div') | ||
39 | rootElement.innerHTML = html | ||
40 | |||
41 | for (const selector of this.getSupportedTags()) { | ||
42 | rootElement.querySelectorAll(selector) | ||
43 | .forEach((e: HTMLElement) => { | ||
44 | try { | ||
45 | const component = this.execBuilder(selector, e) | ||
46 | |||
47 | this.dynamicElementService.injectElement(e, component) | ||
48 | } catch (err) { | ||
49 | console.error('Cannot inject component %s.', selector, err) | ||
50 | } | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | return rootElement | ||
55 | } | ||
56 | |||
57 | private getSupportedTags () { | ||
58 | return Object.keys(this.builders) | ||
59 | } | ||
60 | |||
61 | private execBuilder (selector: string, el: HTMLElement) { | ||
62 | return this.builders[selector](el) | ||
63 | } | ||
64 | |||
65 | private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') { | ||
66 | const data = el.dataset as EmbedMarkupData | ||
67 | const component = this.dynamicElementService.createElement(EmbedMarkupComponent) | ||
68 | |||
69 | this.dynamicElementService.setModel(component, { uuid: data.uuid, type }) | ||
70 | |||
71 | return component | ||
72 | } | ||
73 | |||
74 | private videoMiniatureBuilder (el: HTMLElement) { | ||
75 | const data = el.dataset as VideoMiniatureMarkupData | ||
76 | const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent) | ||
77 | |||
78 | this.dynamicElementService.setModel(component, { uuid: data.uuid }) | ||
79 | |||
80 | return component | ||
81 | } | ||
82 | |||
83 | private playlistMiniatureBuilder (el: HTMLElement) { | ||
84 | const data = el.dataset as PlaylistMiniatureMarkupData | ||
85 | const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent) | ||
86 | |||
87 | this.dynamicElementService.setModel(component, { uuid: data.uuid }) | ||
88 | |||
89 | return component | ||
90 | } | ||
91 | |||
92 | private channelMiniatureBuilder (el: HTMLElement) { | ||
93 | const data = el.dataset as ChannelMiniatureMarkupData | ||
94 | const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent) | ||
95 | |||
96 | this.dynamicElementService.setModel(component, { name: data.name }) | ||
97 | |||
98 | return component | ||
99 | } | ||
100 | |||
101 | private videosListBuilder (el: HTMLElement) { | ||
102 | const data = el.dataset as VideosListMarkupData | ||
103 | const component = this.dynamicElementService.createElement(VideosListMarkupComponent) | ||
104 | |||
105 | const model = { | ||
106 | title: data.title, | ||
107 | description: data.description, | ||
108 | sort: data.sort, | ||
109 | categoryOneOf: this.buildArrayNumber(data.categoryOneOf), | ||
110 | languageOneOf: this.buildArrayString(data.languageOneOf), | ||
111 | count: this.buildNumber(data.count) || 10 | ||
112 | } | ||
113 | |||
114 | this.dynamicElementService.setModel(component, model) | ||
115 | |||
116 | return component | ||
117 | } | ||
118 | |||
119 | private buildNumber (value: string) { | ||
120 | if (!value) return undefined | ||
121 | |||
122 | return parseInt(value, 10) | ||
123 | } | ||
124 | |||
125 | private buildArrayNumber (value: string) { | ||
126 | if (!value) return undefined | ||
127 | |||
128 | return value.split(',').map(v => parseInt(v, 10)) | ||
129 | } | ||
130 | |||
131 | private buildArrayString (value: string) { | ||
132 | if (!value) return undefined | ||
133 | |||
134 | return value.split(',') | ||
135 | } | ||
136 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts b/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts new file mode 100644 index 000000000..e967e30ac --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts | |||
@@ -0,0 +1,57 @@ | |||
1 | import { | ||
2 | ApplicationRef, | ||
3 | ComponentFactoryResolver, | ||
4 | ComponentRef, | ||
5 | EmbeddedViewRef, | ||
6 | Injectable, | ||
7 | Injector, | ||
8 | OnChanges, | ||
9 | SimpleChange, | ||
10 | SimpleChanges, | ||
11 | Type | ||
12 | } from '@angular/core' | ||
13 | |||
14 | @Injectable() | ||
15 | export class DynamicElementService { | ||
16 | |||
17 | constructor ( | ||
18 | private injector: Injector, | ||
19 | private applicationRef: ApplicationRef, | ||
20 | private componentFactoryResolver: ComponentFactoryResolver | ||
21 | ) { } | ||
22 | |||
23 | createElement <T> (ofComponent: Type<T>) { | ||
24 | const div = document.createElement('div') | ||
25 | |||
26 | const component = this.componentFactoryResolver.resolveComponentFactory(ofComponent) | ||
27 | .create(this.injector, [], div) | ||
28 | |||
29 | return component | ||
30 | } | ||
31 | |||
32 | injectElement <T> (wrapper: HTMLElement, componentRef: ComponentRef<T>) { | ||
33 | const hostView = componentRef.hostView as EmbeddedViewRef<any> | ||
34 | |||
35 | this.applicationRef.attachView(hostView) | ||
36 | wrapper.appendChild(hostView.rootNodes[0]) | ||
37 | } | ||
38 | |||
39 | setModel <T> (componentRef: ComponentRef<T>, attributes: Partial<T>) { | ||
40 | const changes: SimpleChanges = {} | ||
41 | |||
42 | for (const key of Object.keys(attributes)) { | ||
43 | const previousValue = componentRef.instance[key] | ||
44 | const newValue = attributes[key] | ||
45 | |||
46 | componentRef.instance[key] = newValue | ||
47 | changes[key] = new SimpleChange(previousValue, newValue, previousValue === undefined) | ||
48 | } | ||
49 | |||
50 | const component = componentRef.instance | ||
51 | if (typeof (component as unknown as OnChanges).ngOnChanges === 'function') { | ||
52 | (component as unknown as OnChanges).ngOnChanges(changes) | ||
53 | } | ||
54 | |||
55 | componentRef.changeDetectorRef.detectChanges() | ||
56 | } | ||
57 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/embed-markup.component.ts b/client/src/app/shared/shared-custom-markup/embed-markup.component.ts new file mode 100644 index 000000000..a854d89f6 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/embed-markup.component.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' | ||
2 | import { environment } from 'src/environments/environment' | ||
3 | import { Component, ElementRef, Input, OnInit } from '@angular/core' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-embed-markup', | ||
7 | template: '' | ||
8 | }) | ||
9 | export class EmbedMarkupComponent implements OnInit { | ||
10 | @Input() uuid: string | ||
11 | @Input() type: 'video' | 'playlist' = 'video' | ||
12 | |||
13 | constructor (private el: ElementRef) { } | ||
14 | |||
15 | ngOnInit () { | ||
16 | const link = this.type === 'video' | ||
17 | ? buildVideoLink({ baseUrl: `${environment.originServerUrl}/videos/embed/${this.uuid}` }) | ||
18 | : buildPlaylistLink({ baseUrl: `${environment.originServerUrl}/video-playlists/embed/${this.uuid}` }) | ||
19 | |||
20 | this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid) | ||
21 | } | ||
22 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/index.ts b/client/src/app/shared/shared-custom-markup/index.ts new file mode 100644 index 000000000..14bde3ea9 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './custom-markup.service' | ||
2 | export * from './dynamic-element.service' | ||
3 | export * from './shared-custom-markup.module' | ||
diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html new file mode 100644 index 000000000..4e1d1a13f --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html | |||
@@ -0,0 +1,2 @@ | |||
1 | <my-video-playlist-miniature *ngIf="playlist" [playlist]="playlist"> | ||
2 | </my-video-playlist-miniature> | ||
diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss new file mode 100644 index 000000000..281cef726 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss | |||
@@ -0,0 +1,7 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | my-video-playlist-miniature { | ||
5 | display: inline-block; | ||
6 | width: $video-thumbnail-width; | ||
7 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts new file mode 100644 index 000000000..7aee450f1 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts | |||
@@ -0,0 +1,38 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { MiniatureDisplayOptions } from '../shared-video-miniature' | ||
3 | import { VideoPlaylist, VideoPlaylistService } from '../shared-video-playlist' | ||
4 | |||
5 | /* | ||
6 | * Markup component that creates a playlist miniature only | ||
7 | */ | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-playlist-miniature-markup', | ||
11 | templateUrl: 'playlist-miniature-markup.component.html', | ||
12 | styleUrls: [ 'playlist-miniature-markup.component.scss' ] | ||
13 | }) | ||
14 | export class PlaylistMiniatureMarkupComponent implements OnInit { | ||
15 | @Input() uuid: string | ||
16 | |||
17 | playlist: VideoPlaylist | ||
18 | |||
19 | displayOptions: MiniatureDisplayOptions = { | ||
20 | date: true, | ||
21 | views: true, | ||
22 | by: true, | ||
23 | avatar: false, | ||
24 | privacyLabel: false, | ||
25 | privacyText: false, | ||
26 | state: false, | ||
27 | blacklistInfo: false | ||
28 | } | ||
29 | |||
30 | constructor ( | ||
31 | private playlistService: VideoPlaylistService | ||
32 | ) { } | ||
33 | |||
34 | ngOnInit () { | ||
35 | this.playlistService.getVideoPlaylist(this.uuid) | ||
36 | .subscribe(playlist => this.playlist = playlist) | ||
37 | } | ||
38 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts new file mode 100644 index 000000000..4bbb71588 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts | |||
@@ -0,0 +1,49 @@ | |||
1 | |||
2 | import { CommonModule } from '@angular/common' | ||
3 | import { NgModule } from '@angular/core' | ||
4 | import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' | ||
5 | import { SharedGlobalIconModule } from '../shared-icons' | ||
6 | import { SharedMainModule } from '../shared-main' | ||
7 | import { SharedVideoMiniatureModule } from '../shared-video-miniature' | ||
8 | import { SharedVideoPlaylistModule } from '../shared-video-playlist' | ||
9 | import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component' | ||
10 | import { CustomMarkupService } from './custom-markup.service' | ||
11 | import { DynamicElementService } from './dynamic-element.service' | ||
12 | import { EmbedMarkupComponent } from './embed-markup.component' | ||
13 | import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component' | ||
14 | import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component' | ||
15 | import { VideosListMarkupComponent } from './videos-list-markup.component' | ||
16 | |||
17 | @NgModule({ | ||
18 | imports: [ | ||
19 | CommonModule, | ||
20 | |||
21 | SharedMainModule, | ||
22 | SharedGlobalIconModule, | ||
23 | SharedVideoMiniatureModule, | ||
24 | SharedVideoPlaylistModule, | ||
25 | SharedActorImageModule | ||
26 | ], | ||
27 | |||
28 | declarations: [ | ||
29 | VideoMiniatureMarkupComponent, | ||
30 | PlaylistMiniatureMarkupComponent, | ||
31 | ChannelMiniatureMarkupComponent, | ||
32 | EmbedMarkupComponent, | ||
33 | VideosListMarkupComponent | ||
34 | ], | ||
35 | |||
36 | exports: [ | ||
37 | VideoMiniatureMarkupComponent, | ||
38 | PlaylistMiniatureMarkupComponent, | ||
39 | ChannelMiniatureMarkupComponent, | ||
40 | VideosListMarkupComponent, | ||
41 | EmbedMarkupComponent | ||
42 | ], | ||
43 | |||
44 | providers: [ | ||
45 | CustomMarkupService, | ||
46 | DynamicElementService | ||
47 | ] | ||
48 | }) | ||
49 | export class SharedCustomMarkupModule { } | ||
diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html new file mode 100644 index 000000000..9b4930b6d --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html | |||
@@ -0,0 +1,6 @@ | |||
1 | <my-video-miniature | ||
2 | *ngIf="video" | ||
3 | [video]="video" [user]="getUser()" [displayAsRow]="false" | ||
4 | [displayVideoActions]="false" [displayOptions]="displayOptions" | ||
5 | > | ||
6 | </my-video-miniature> | ||
diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss new file mode 100644 index 000000000..81e265f29 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss | |||
@@ -0,0 +1,7 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | my-video-miniature { | ||
5 | display: inline-block; | ||
6 | width: $video-thumbnail-width; | ||
7 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts new file mode 100644 index 000000000..79add0c3b --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts | |||
@@ -0,0 +1,44 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { Video, VideoService } from '../shared-main' | ||
4 | import { MiniatureDisplayOptions } from '../shared-video-miniature' | ||
5 | |||
6 | /* | ||
7 | * Markup component that creates a video miniature only | ||
8 | */ | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-video-miniature-markup', | ||
12 | templateUrl: 'video-miniature-markup.component.html', | ||
13 | styleUrls: [ 'video-miniature-markup.component.scss' ] | ||
14 | }) | ||
15 | export class VideoMiniatureMarkupComponent implements OnInit { | ||
16 | @Input() uuid: string | ||
17 | |||
18 | video: Video | ||
19 | |||
20 | displayOptions: MiniatureDisplayOptions = { | ||
21 | date: true, | ||
22 | views: true, | ||
23 | by: true, | ||
24 | avatar: false, | ||
25 | privacyLabel: false, | ||
26 | privacyText: false, | ||
27 | state: false, | ||
28 | blacklistInfo: false | ||
29 | } | ||
30 | |||
31 | constructor ( | ||
32 | private auth: AuthService, | ||
33 | private videoService: VideoService | ||
34 | ) { } | ||
35 | |||
36 | getUser () { | ||
37 | return this.auth.getUser() | ||
38 | } | ||
39 | |||
40 | ngOnInit () { | ||
41 | this.videoService.getVideo({ videoId: this.uuid }) | ||
42 | .subscribe(video => this.video = video) | ||
43 | } | ||
44 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html new file mode 100644 index 000000000..501f35e04 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html | |||
@@ -0,0 +1,13 @@ | |||
1 | <div class="root"> | ||
2 | <h4 *ngIf="title">{{ title }}</h4> | ||
3 | <div *ngIf="description" class="description">{{ description }}</div> | ||
4 | |||
5 | <div class="videos"> | ||
6 | <my-video-miniature | ||
7 | *ngFor="let video of videos" | ||
8 | [video]="video" [user]="getUser()" [displayAsRow]="false" | ||
9 | [displayVideoActions]="false" [displayOptions]="displayOptions" | ||
10 | > | ||
11 | </my-video-miniature> | ||
12 | </div> | ||
13 | </div> | ||
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss new file mode 100644 index 000000000..dcd931090 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss | |||
@@ -0,0 +1,9 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | my-video-miniature { | ||
5 | margin-right: 15px; | ||
6 | display: inline-block; | ||
7 | min-width: $video-thumbnail-width; | ||
8 | max-width: $video-thumbnail-width; | ||
9 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts new file mode 100644 index 000000000..cc25d0a51 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts | |||
@@ -0,0 +1,60 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { VideoSortField } from '@shared/models' | ||
4 | import { Video, VideoService } from '../shared-main' | ||
5 | import { MiniatureDisplayOptions } from '../shared-video-miniature' | ||
6 | |||
7 | /* | ||
8 | * Markup component list videos depending on criterias | ||
9 | */ | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-videos-list-markup', | ||
13 | templateUrl: 'videos-list-markup.component.html', | ||
14 | styleUrls: [ 'videos-list-markup.component.scss' ] | ||
15 | }) | ||
16 | export class VideosListMarkupComponent implements OnInit { | ||
17 | @Input() title: string | ||
18 | @Input() description: string | ||
19 | @Input() sort = '-publishedAt' | ||
20 | @Input() categoryOneOf: number[] | ||
21 | @Input() languageOneOf: string[] | ||
22 | @Input() count = 10 | ||
23 | |||
24 | videos: Video[] | ||
25 | |||
26 | displayOptions: MiniatureDisplayOptions = { | ||
27 | date: true, | ||
28 | views: true, | ||
29 | by: true, | ||
30 | avatar: false, | ||
31 | privacyLabel: false, | ||
32 | privacyText: false, | ||
33 | state: false, | ||
34 | blacklistInfo: false | ||
35 | } | ||
36 | |||
37 | constructor ( | ||
38 | private auth: AuthService, | ||
39 | private videoService: VideoService | ||
40 | ) { } | ||
41 | |||
42 | getUser () { | ||
43 | return this.auth.getUser() | ||
44 | } | ||
45 | |||
46 | ngOnInit () { | ||
47 | const options = { | ||
48 | videoPagination: { | ||
49 | currentPage: 1, | ||
50 | itemsPerPage: this.count | ||
51 | }, | ||
52 | categoryOneOf: this.categoryOneOf, | ||
53 | languageOneOf: this.languageOneOf, | ||
54 | sort: this.sort as VideoSortField | ||
55 | } | ||
56 | |||
57 | this.videoService.getVideos(options) | ||
58 | .subscribe(({ data }) => this.videos = data) | ||
59 | } | ||
60 | } | ||
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.html b/client/src/app/shared/shared-forms/markdown-textarea.component.html index 513b543cd..6e70e2f37 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.html +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.html | |||
@@ -19,6 +19,7 @@ | |||
19 | <a ngbNavLink i18n>Complete preview</a> | 19 | <a ngbNavLink i18n>Complete preview</a> |
20 | 20 | ||
21 | <ng-template ngbNavContent> | 21 | <ng-template ngbNavContent> |
22 | <div #previewElement></div> | ||
22 | <div [innerHTML]="previewHTML"></div> | 23 | <div [innerHTML]="previewHTML"></div> |
23 | </ng-template> | 24 | </ng-template> |
24 | </ng-container> | 25 | </ng-container> |
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts index 9b3ab9cf3..a233a4205 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { ViewportScroller } from '@angular/common' | ||
2 | import truncate from 'lodash-es/truncate' | 1 | import truncate from 'lodash-es/truncate' |
3 | import { Subject } from 'rxjs' | 2 | import { Subject } from 'rxjs' |
4 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 3 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
4 | import { ViewportScroller } from '@angular/common' | ||
5 | import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core' | 5 | import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core' |
6 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 6 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
7 | import { SafeHtml } from '@angular/platform-browser' | ||
7 | import { MarkdownService, ScreenService } from '@app/core' | 8 | import { MarkdownService, ScreenService } from '@app/core' |
8 | 9 | ||
9 | @Component({ | 10 | @Component({ |
@@ -21,18 +22,27 @@ import { MarkdownService, ScreenService } from '@app/core' | |||
21 | 22 | ||
22 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | 23 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { |
23 | @Input() content = '' | 24 | @Input() content = '' |
25 | |||
24 | @Input() classes: string[] | { [klass: string]: any[] | any } = [] | 26 | @Input() classes: string[] | { [klass: string]: any[] | any } = [] |
27 | |||
25 | @Input() textareaMaxWidth = '100%' | 28 | @Input() textareaMaxWidth = '100%' |
26 | @Input() textareaHeight = '150px' | 29 | @Input() textareaHeight = '150px' |
30 | |||
27 | @Input() truncate: number | 31 | @Input() truncate: number |
32 | |||
28 | @Input() markdownType: 'text' | 'enhanced' = 'text' | 33 | @Input() markdownType: 'text' | 'enhanced' = 'text' |
34 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> | ||
35 | |||
29 | @Input() markdownVideo = false | 36 | @Input() markdownVideo = false |
37 | |||
30 | @Input() name = 'description' | 38 | @Input() name = 'description' |
31 | 39 | ||
32 | @ViewChild('textarea') textareaElement: ElementRef | 40 | @ViewChild('textarea') textareaElement: ElementRef |
41 | @ViewChild('previewElement') previewElement: ElementRef | ||
42 | |||
43 | truncatedPreviewHTML: SafeHtml | string = '' | ||
44 | previewHTML: SafeHtml | string = '' | ||
33 | 45 | ||
34 | truncatedPreviewHTML = '' | ||
35 | previewHTML = '' | ||
36 | isMaximized = false | 46 | isMaximized = false |
37 | 47 | ||
38 | maximizeInText = $localize`Maximize editor` | 48 | maximizeInText = $localize`Maximize editor` |
@@ -115,10 +125,31 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
115 | } | 125 | } |
116 | 126 | ||
117 | private async markdownRender (text: string) { | 127 | private async markdownRender (text: string) { |
118 | const html = this.markdownType === 'text' ? | 128 | let html: string |
119 | await this.markdownService.textMarkdownToHTML(text) : | 129 | |
120 | await this.markdownService.enhancedMarkdownToHTML(text) | 130 | if (this.customMarkdownRenderer) { |
131 | const result = await this.customMarkdownRenderer(text) | ||
132 | |||
133 | if (result instanceof HTMLElement) { | ||
134 | html = '' | ||
135 | |||
136 | const wrapperElement = this.previewElement.nativeElement as HTMLElement | ||
137 | wrapperElement.innerHTML = '' | ||
138 | wrapperElement.appendChild(result) | ||
139 | return | ||
140 | } | ||
141 | |||
142 | html = result | ||
143 | } else if (this.markdownType === 'text') { | ||
144 | html = await this.markdownService.textMarkdownToHTML(text) | ||
145 | } else { | ||
146 | html = await this.markdownService.enhancedMarkdownToHTML(text) | ||
147 | } | ||
148 | |||
149 | if (this.markdownVideo) { | ||
150 | html = this.markdownService.processVideoTimestamps(html) | ||
151 | } | ||
121 | 152 | ||
122 | return this.markdownVideo ? this.markdownService.processVideoTimestamps(html) : html | 153 | return html |
123 | } | 154 | } |
124 | } | 155 | } |
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index 3af517927..a4dd72db6 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts | |||
@@ -72,6 +72,7 @@ const icons = { | |||
72 | 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, | 72 | 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, |
73 | 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default, | 73 | 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default, |
74 | 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default, | 74 | 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default, |
75 | 'octagon': require('!!raw-loader?!../../../assets/images/feather/octagon.svg').default, | ||
75 | 'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default | 76 | 'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default |
76 | } | 77 | } |
77 | 78 | ||
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts index 6d9f0ee65..7b5611f35 100644 --- a/client/src/app/shared/shared-main/account/account.model.ts +++ b/client/src/app/shared/shared-main/account/account.model.ts | |||
@@ -4,8 +4,12 @@ import { Actor } from './actor.model' | |||
4 | export class Account extends Actor implements ServerAccount { | 4 | export class Account extends Actor implements ServerAccount { |
5 | displayName: string | 5 | displayName: string |
6 | description: string | 6 | description: string |
7 | |||
8 | updatedAt: Date | string | ||
9 | |||
7 | nameWithHost: string | 10 | nameWithHost: string |
8 | nameWithHostForced: string | 11 | nameWithHostForced: string |
12 | |||
9 | mutedByUser: boolean | 13 | mutedByUser: boolean |
10 | mutedByInstance: boolean | 14 | mutedByInstance: boolean |
11 | mutedServerByUser: boolean | 15 | mutedServerByUser: boolean |
@@ -30,6 +34,8 @@ export class Account extends Actor implements ServerAccount { | |||
30 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) | 34 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) |
31 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) | 35 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) |
32 | 36 | ||
37 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
38 | |||
33 | this.mutedByUser = false | 39 | this.mutedByUser = false |
34 | this.mutedByInstance = false | 40 | this.mutedByInstance = false |
35 | this.mutedServerByUser = false | 41 | this.mutedServerByUser = false |
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 6ba0bb09e..2fccc472a 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts | |||
@@ -12,7 +12,6 @@ export abstract class Actor implements ServerActor { | |||
12 | followersCount: number | 12 | followersCount: number |
13 | 13 | ||
14 | createdAt: Date | string | 14 | createdAt: Date | string |
15 | updatedAt: Date | string | ||
16 | 15 | ||
17 | avatar: ActorImage | 16 | avatar: ActorImage |
18 | 17 | ||
@@ -55,7 +54,6 @@ export abstract class Actor implements ServerActor { | |||
55 | this.followersCount = hash.followersCount | 54 | this.followersCount = hash.followersCount |
56 | 55 | ||
57 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) | 56 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) |
58 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
59 | 57 | ||
60 | this.avatar = hash.avatar | 58 | this.avatar = hash.avatar |
61 | this.isLocal = Actor.IS_LOCAL(this.host) | 59 | this.isLocal = Actor.IS_LOCAL(this.host) |
diff --git a/client/src/app/shared/shared-main/angular/from-now.pipe.ts b/client/src/app/shared/shared-main/angular/from-now.pipe.ts index 5e7832807..d62c1f88e 100644 --- a/client/src/app/shared/shared-main/angular/from-now.pipe.ts +++ b/client/src/app/shared/shared-main/angular/from-now.pipe.ts | |||
@@ -3,32 +3,37 @@ import { Pipe, PipeTransform } from '@angular/core' | |||
3 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site | 3 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site |
4 | @Pipe({ name: 'myFromNow' }) | 4 | @Pipe({ name: 'myFromNow' }) |
5 | export class FromNowPipe implements PipeTransform { | 5 | export class FromNowPipe implements PipeTransform { |
6 | |||
7 | transform (arg: number | Date | string) { | 6 | transform (arg: number | Date | string) { |
8 | const argDate = new Date(arg) | 7 | const argDate = new Date(arg) |
9 | const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) | 8 | const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) |
10 | 9 | ||
11 | let interval = Math.round(seconds / 31536000) | 10 | let interval = Math.floor(seconds / 31536000) |
12 | if (interval > 1) return $localize`${interval} years ago` | 11 | if (interval > 1) return $localize`${interval} years ago` |
13 | if (interval === 1) return $localize`${interval} year ago` | 12 | if (interval === 1) return $localize`1 year ago` |
14 | 13 | ||
15 | interval = Math.round(seconds / 2592000) | 14 | interval = Math.floor(seconds / 2419200) |
15 | // 12 months = 360 days, but a year ~ 365 days | ||
16 | // Display "1 year ago" rather than "12 months ago" | ||
17 | if (interval >= 12) return $localize`1 year ago` | ||
16 | if (interval > 1) return $localize`${interval} months ago` | 18 | if (interval > 1) return $localize`${interval} months ago` |
17 | if (interval === 1) return $localize`${interval} month ago` | 19 | if (interval === 1) return $localize`1 month ago` |
18 | 20 | ||
19 | interval = Math.round(seconds / 604800) | 21 | interval = Math.floor(seconds / 604800) |
22 | // 4 weeks ~ 28 days, but our month is 30 days | ||
23 | // Display "1 month ago" rather than "4 weeks ago" | ||
24 | if (interval >= 4) return $localize`1 month ago` | ||
20 | if (interval > 1) return $localize`${interval} weeks ago` | 25 | if (interval > 1) return $localize`${interval} weeks ago` |
21 | if (interval === 1) return $localize`${interval} week ago` | 26 | if (interval === 1) return $localize`1 week ago` |
22 | 27 | ||
23 | interval = Math.round(seconds / 86400) | 28 | interval = Math.floor(seconds / 86400) |
24 | if (interval > 1) return $localize`${interval} days ago` | 29 | if (interval > 1) return $localize`${interval} days ago` |
25 | if (interval === 1) return $localize`${interval} day ago` | 30 | if (interval === 1) return $localize`1 day ago` |
26 | 31 | ||
27 | interval = Math.round(seconds / 3600) | 32 | interval = Math.floor(seconds / 3600) |
28 | if (interval > 1) return $localize`${interval} hours ago` | 33 | if (interval > 1) return $localize`${interval} hours ago` |
29 | if (interval === 1) return $localize`${interval} hour ago` | 34 | if (interval === 1) return $localize`1 hour ago` |
30 | 35 | ||
31 | interval = Math.round(seconds / 60) | 36 | interval = Math.floor(seconds / 60) |
32 | if (interval >= 1) return $localize`${interval} min ago` | 37 | if (interval >= 1) return $localize`${interval} min ago` |
33 | 38 | ||
34 | return $localize`just now` | 39 | return $localize`just now` |
diff --git a/client/src/app/shared/shared-main/buttons/button.component.scss b/client/src/app/shared/shared-main/buttons/button.component.scss index 09b5f95d7..22b24c853 100644 --- a/client/src/app/shared/shared-main/buttons/button.component.scss +++ b/client/src/app/shared/shared-main/buttons/button.component.scss | |||
@@ -30,7 +30,7 @@ span[class$=-button] { | |||
30 | 30 | ||
31 | .action-button { | 31 | .action-button { |
32 | @include peertube-button-link; | 32 | @include peertube-button-link; |
33 | @include button-with-icon(21px, 0, -1px); | 33 | @include button-with-icon(21px); |
34 | } | 34 | } |
35 | 35 | ||
36 | .orange-button { | 36 | .orange-button { |
diff --git a/client/src/app/shared/shared-main/custom-page/custom-page.service.ts b/client/src/app/shared/shared-main/custom-page/custom-page.service.ts new file mode 100644 index 000000000..e5c2b3cd4 --- /dev/null +++ b/client/src/app/shared/shared-main/custom-page/custom-page.service.ts | |||
@@ -0,0 +1,38 @@ | |||
1 | import { of } from 'rxjs' | ||
2 | import { catchError, map } from 'rxjs/operators' | ||
3 | import { HttpClient } from '@angular/common/http' | ||
4 | import { Injectable } from '@angular/core' | ||
5 | import { RestExtractor } from '@app/core' | ||
6 | import { CustomPage } from '@shared/models' | ||
7 | import { environment } from '../../../../environments/environment' | ||
8 | |||
9 | @Injectable() | ||
10 | export class CustomPageService { | ||
11 | static BASE_INSTANCE_HOMEPAGE_URL = environment.apiUrl + '/api/v1/custom-pages/homepage/instance' | ||
12 | |||
13 | constructor ( | ||
14 | private authHttp: HttpClient, | ||
15 | private restExtractor: RestExtractor | ||
16 | ) { } | ||
17 | |||
18 | getInstanceHomepage () { | ||
19 | return this.authHttp.get<CustomPage>(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL) | ||
20 | .pipe( | ||
21 | catchError(err => { | ||
22 | if (err.status === 404) { | ||
23 | return of({ content: '' }) | ||
24 | } | ||
25 | |||
26 | this.restExtractor.handleError(err) | ||
27 | }) | ||
28 | ) | ||
29 | } | ||
30 | |||
31 | updateInstanceHomepage (content: string) { | ||
32 | return this.authHttp.put(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL, { content }) | ||
33 | .pipe( | ||
34 | map(this.restExtractor.extractDataBool), | ||
35 | catchError(err => this.restExtractor.handleError(err)) | ||
36 | ) | ||
37 | } | ||
38 | } | ||
diff --git a/client/src/app/shared/shared-main/custom-page/index.ts b/client/src/app/shared/shared-main/custom-page/index.ts new file mode 100644 index 000000000..7269ece95 --- /dev/null +++ b/client/src/app/shared/shared-main/custom-page/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './custom-page.service' | |||
diff --git a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts index 93ba9fb9b..4d5381e8d 100644 --- a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts +++ b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts | |||
@@ -3,7 +3,8 @@ import { PluginElementPlaceholder } from '@shared/models' | |||
3 | 3 | ||
4 | @Component({ | 4 | @Component({ |
5 | selector: 'my-plugin-placeholder', | 5 | selector: 'my-plugin-placeholder', |
6 | template: '<div [id]="getId()"></div>' | 6 | template: '<div [id]="getId()"></div>', |
7 | styles: [ 'div { height: 100%; }' ] | ||
7 | }) | 8 | }) |
8 | 9 | ||
9 | export class PluginPlaceholderComponent { | 10 | export class PluginPlaceholderComponent { |
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 05a5d77c7..f06f25ca5 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -29,6 +29,7 @@ import { | |||
29 | } from './angular' | 29 | } from './angular' |
30 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | 30 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' |
31 | import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' | 31 | import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' |
32 | import { CustomPageService } from './custom-page' | ||
32 | import { DateToggleComponent } from './date' | 33 | import { DateToggleComponent } from './date' |
33 | import { FeedComponent } from './feeds' | 34 | import { FeedComponent } from './feeds' |
34 | import { LoaderComponent, SmallLoaderComponent } from './loaders' | 35 | import { LoaderComponent, SmallLoaderComponent } from './loaders' |
@@ -172,7 +173,9 @@ import { VideoChannelService } from './video-channel' | |||
172 | 173 | ||
173 | VideoCaptionService, | 174 | VideoCaptionService, |
174 | 175 | ||
175 | VideoChannelService | 176 | VideoChannelService, |
177 | |||
178 | CustomPageService | ||
176 | ] | 179 | ] |
177 | }) | 180 | }) |
178 | export class SharedMainModule { } | 181 | export class SharedMainModule { } |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts index c40dd5311..a9dcf2fa2 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts | |||
@@ -16,6 +16,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
16 | banner: ActorImage | 16 | banner: ActorImage |
17 | bannerUrl: string | 17 | bannerUrl: string |
18 | 18 | ||
19 | updatedAt: Date | string | ||
20 | |||
19 | ownerAccount?: ServerAccount | 21 | ownerAccount?: ServerAccount |
20 | ownerBy?: string | 22 | ownerBy?: string |
21 | 23 | ||
@@ -59,6 +61,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
59 | 61 | ||
60 | this.videosCount = hash.videosCount | 62 | this.videosCount = hash.videosCount |
61 | 63 | ||
64 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
65 | |||
62 | if (hash.viewsPerDay) { | 66 | if (hash.viewsPerDay) { |
63 | this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) })) | 67 | this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) })) |
64 | } | 68 | } |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts index e65261763..a89f1065a 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts | |||
@@ -40,23 +40,24 @@ export class VideoChannelService { | |||
40 | ) | 40 | ) |
41 | } | 41 | } |
42 | 42 | ||
43 | listAccountVideoChannels ( | 43 | listAccountVideoChannels (options: { |
44 | account: Account, | 44 | account: Account |
45 | componentPagination?: ComponentPaginationLight, | 45 | componentPagination?: ComponentPaginationLight |
46 | withStats = false, | 46 | withStats?: boolean |
47 | sort?: string | ||
47 | search?: string | 48 | search?: string |
48 | ): Observable<ResultList<VideoChannel>> { | 49 | }): Observable<ResultList<VideoChannel>> { |
50 | const { account, componentPagination, withStats = false, sort, search } = options | ||
51 | |||
49 | const pagination = componentPagination | 52 | const pagination = componentPagination |
50 | ? this.restService.componentPaginationToRestPagination(componentPagination) | 53 | ? this.restService.componentPaginationToRestPagination(componentPagination) |
51 | : { start: 0, count: 20 } | 54 | : { start: 0, count: 20 } |
52 | 55 | ||
53 | let params = new HttpParams() | 56 | let params = new HttpParams() |
54 | params = this.restService.addRestGetParams(params, pagination) | 57 | params = this.restService.addRestGetParams(params, pagination, sort) |
55 | params = params.set('withStats', withStats + '') | 58 | params = params.set('withStats', withStats + '') |
56 | 59 | ||
57 | if (search) { | 60 | if (search) params = params.set('search', search) |
58 | params = params.set('search', search) | ||
59 | } | ||
60 | 61 | ||
61 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' | 62 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' |
62 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) | 63 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) |
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts index 0e3924841..2c83f53b6 100644 --- a/client/src/app/shared/shared-search/advanced-search.model.ts +++ b/client/src/app/shared/shared-search/advanced-search.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { BooleanBothQuery, SearchTargetType } from '@shared/models' | 1 | import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models' |
2 | 2 | ||
3 | export class AdvancedSearch { | 3 | export class AdvancedSearch { |
4 | startDate: string // ISO 8601 | 4 | startDate: string // ISO 8601 |
@@ -21,6 +21,8 @@ export class AdvancedSearch { | |||
21 | durationMin: number // seconds | 21 | durationMin: number // seconds |
22 | durationMax: number // seconds | 22 | durationMax: number // seconds |
23 | 23 | ||
24 | isLive: BooleanQuery | ||
25 | |||
24 | sort: string | 26 | sort: string |
25 | 27 | ||
26 | searchTarget: SearchTargetType | 28 | searchTarget: SearchTargetType |
@@ -41,6 +43,8 @@ export class AdvancedSearch { | |||
41 | tagsOneOf?: any | 43 | tagsOneOf?: any |
42 | tagsAllOf?: any | 44 | tagsAllOf?: any |
43 | 45 | ||
46 | isLive?: BooleanQuery | ||
47 | |||
44 | durationMin?: string | 48 | durationMin?: string |
45 | durationMax?: string | 49 | durationMax?: string |
46 | sort?: string | 50 | sort?: string |
@@ -54,6 +58,8 @@ export class AdvancedSearch { | |||
54 | this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined | 58 | this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined |
55 | 59 | ||
56 | this.nsfw = options.nsfw || undefined | 60 | this.nsfw = options.nsfw || undefined |
61 | this.isLive = options.isLive || undefined | ||
62 | |||
57 | this.categoryOneOf = options.categoryOneOf || undefined | 63 | this.categoryOneOf = options.categoryOneOf || undefined |
58 | this.licenceOneOf = options.licenceOneOf || undefined | 64 | this.licenceOneOf = options.licenceOneOf || undefined |
59 | this.languageOneOf = options.languageOneOf || undefined | 65 | this.languageOneOf = options.languageOneOf || undefined |
@@ -94,6 +100,7 @@ export class AdvancedSearch { | |||
94 | this.tagsAllOf = undefined | 100 | this.tagsAllOf = undefined |
95 | this.durationMin = undefined | 101 | this.durationMin = undefined |
96 | this.durationMax = undefined | 102 | this.durationMax = undefined |
103 | this.isLive = undefined | ||
97 | 104 | ||
98 | this.sort = '-match' | 105 | this.sort = '-match' |
99 | } | 106 | } |
@@ -112,12 +119,16 @@ export class AdvancedSearch { | |||
112 | tagsAllOf: this.tagsAllOf, | 119 | tagsAllOf: this.tagsAllOf, |
113 | durationMin: this.durationMin, | 120 | durationMin: this.durationMin, |
114 | durationMax: this.durationMax, | 121 | durationMax: this.durationMax, |
122 | isLive: this.isLive, | ||
115 | sort: this.sort, | 123 | sort: this.sort, |
116 | searchTarget: this.searchTarget | 124 | searchTarget: this.searchTarget |
117 | } | 125 | } |
118 | } | 126 | } |
119 | 127 | ||
120 | toAPIObject () { | 128 | toAPIObject (): VideosSearchQuery { |
129 | let isLive: boolean | ||
130 | if (this.isLive) isLive = this.isLive === 'true' | ||
131 | |||
121 | return { | 132 | return { |
122 | startDate: this.startDate, | 133 | startDate: this.startDate, |
123 | endDate: this.endDate, | 134 | endDate: this.endDate, |
@@ -131,6 +142,7 @@ export class AdvancedSearch { | |||
131 | tagsAllOf: this.tagsAllOf, | 142 | tagsAllOf: this.tagsAllOf, |
132 | durationMin: this.durationMin, | 143 | durationMin: this.durationMin, |
133 | durationMax: this.durationMax, | 144 | durationMax: this.durationMax, |
145 | isLive, | ||
134 | sort: this.sort, | 146 | sort: this.sort, |
135 | searchTarget: this.searchTarget | 147 | searchTarget: this.searchTarget |
136 | } | 148 | } |
diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html index 75cfc918b..d8699ff69 100644 --- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html +++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html | |||
@@ -40,7 +40,7 @@ | |||
40 | </ng-container> | 40 | </ng-container> |
41 | 41 | ||
42 | <div | 42 | <div |
43 | class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right" | 43 | class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right bottom-left" |
44 | role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label | 44 | role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label |
45 | > | 45 | > |
46 | <button class="btn btn-sm dropdown-toggle-split" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label> | 46 | <button class="btn btn-sm dropdown-toggle-split" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index 5df89d019..0bbdff1e6 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss | |||
@@ -95,6 +95,7 @@ my-actor-avatar { | |||
95 | .video-bottom { | 95 | .video-bottom { |
96 | display: flex; | 96 | display: flex; |
97 | width: 100%; | 97 | width: 100%; |
98 | min-width: 1px; | ||
98 | } | 99 | } |
99 | 100 | ||
100 | .video-miniature-name { | 101 | .video-miniature-name { |
@@ -145,6 +146,7 @@ my-actor-avatar { | |||
145 | 146 | ||
146 | .video-bottom { | 147 | .video-bottom { |
147 | display: flex; | 148 | display: flex; |
149 | min-width: 1px; | ||
148 | } | 150 | } |
149 | 151 | ||
150 | // We don't display avatar in row mode | 152 | // We don't display avatar in row mode |