aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html2
-rw-r--r--client/src/app/+about/about-peertube/about-peertube-contributors.component.ts2
-rw-r--r--client/src/app/+about/about-routing.module.ts8
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss2
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts8
-rw-r--r--client/src/app/+accounts/accounts-routing.module.ts2
-rw-r--r--client/src/app/+accounts/accounts.component.ts2
-rw-r--r--client/src/app/+admin/admin-routing.module.ts3
-rw-r--r--client/src/app/+admin/admin.module.ts8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html23
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts26
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html14
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts48
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html28
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts25
-rw-r--r--client/src/app/+admin/config/edit-custom-config/index.ts1
-rw-r--r--client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts3
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.html2
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts3
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts2
-rw-r--r--client/src/app/+home/home-routing.module.ts18
-rw-r--r--client/src/app/+home/home.component.html4
-rw-r--r--client/src/app/+home/home.component.scss3
-rw-r--r--client/src/app/+home/home.component.ts26
-rw-r--r--client/src/app/+home/home.module.ts25
-rw-r--r--client/src/app/+home/index.ts3
-rw-r--r--client/src/app/+login/login-routing.module.ts4
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts5
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.ts4
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts6
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts8
-rw-r--r--client/src/app/+my-library/my-library-routing.module.ts3
-rw-r--r--client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html9
-rw-r--r--client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts16
-rw-r--r--client/src/app/+reset-password/reset-password-routing.module.ts4
-rw-r--r--client/src/app/+search/search-filters.component.html27
-rw-r--r--client/src/app/+search/search-filters.component.ts40
-rw-r--r--client/src/app/+search/search-routing.module.ts4
-rw-r--r--client/src/app/+search/search.component.ts8
-rw-r--r--client/src/app/+signup/+register/register-routing.module.ts3
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-routing.module.ts4
-rw-r--r--client/src/app/+video-channels/video-channels-routing.module.ts2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html4
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss119
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts11
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts48
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts10
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html20
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss4
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts295
-rw-r--r--client/src/app/+videos/+video-edit/video-add-routing.module.ts3
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.html10
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.ts20
-rw-r--r--client/src/app/+videos/+video-edit/video-add.module.ts5
-rw-r--r--client/src/app/+videos/+video-edit/video-update-routing.module.ts3
-rw-r--r--client/src/app/+videos/+video-edit/video-update.resolver.ts18
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment.component.ts2
-rw-r--r--client/src/app/+videos/+video-watch/video-watch-routing.module.ts7
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.scss3
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts6
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.ts12
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending.component.ts7
-rw-r--r--client/src/app/+videos/videos-routing.module.ts2
-rw-r--r--client/src/app/app-routing.module.ts45
-rw-r--r--client/src/app/app.component.scss4
-rw-r--r--client/src/app/app.component.ts4
-rw-r--r--client/src/app/app.module.ts20
-rw-r--r--client/src/app/core/core.module.ts7
-rw-r--r--client/src/app/core/menu/menu.service.ts58
-rw-r--r--client/src/app/core/renderer/html-renderer.service.ts10
-rw-r--r--client/src/app/core/renderer/markdown.service.ts53
-rw-r--r--client/src/app/core/routing/index.ts2
-rw-r--r--client/src/app/core/routing/meta-guard.service.ts23
-rw-r--r--client/src/app/core/routing/meta.service.ts40
-rw-r--r--client/src/app/core/routing/redirect.service.ts35
-rw-r--r--client/src/app/core/server/server.service.ts32
-rw-r--r--client/src/app/core/theme/theme.service.ts14
-rw-r--r--client/src/app/helpers/utils.ts48
-rw-r--r--client/src/app/menu/menu.component.html21
-rw-r--r--client/src/app/menu/menu.component.ts23
-rw-r--r--client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts2
-rw-r--r--client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html8
-rw-r--r--client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss9
-rw-r--r--client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts26
-rw-r--r--client/src/app/shared/shared-custom-markup/custom-markup.service.ts136
-rw-r--r--client/src/app/shared/shared-custom-markup/dynamic-element.service.ts57
-rw-r--r--client/src/app/shared/shared-custom-markup/embed-markup.component.ts22
-rw-r--r--client/src/app/shared/shared-custom-markup/index.ts3
-rw-r--r--client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html2
-rw-r--r--client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss7
-rw-r--r--client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts38
-rw-r--r--client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts49
-rw-r--r--client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html6
-rw-r--r--client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss7
-rw-r--r--client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts44
-rw-r--r--client/src/app/shared/shared-custom-markup/videos-list-markup.component.html13
-rw-r--r--client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss9
-rw-r--r--client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts60
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.html1
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.ts45
-rw-r--r--client/src/app/shared/shared-icons/global-icon.component.ts1
-rw-r--r--client/src/app/shared/shared-main/account/account.model.ts6
-rw-r--r--client/src/app/shared/shared-main/account/actor.model.ts2
-rw-r--r--client/src/app/shared/shared-main/angular/from-now.pipe.ts29
-rw-r--r--client/src/app/shared/shared-main/buttons/button.component.scss2
-rw-r--r--client/src/app/shared/shared-main/custom-page/custom-page.service.ts38
-rw-r--r--client/src/app/shared/shared-main/custom-page/index.ts1
-rw-r--r--client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts3
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts5
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.model.ts4
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts19
-rw-r--r--client/src/app/shared/shared-search/advanced-search.model.ts16
-rw-r--r--client/src/app/shared/shared-user-subscription/subscribe-button.component.html2
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss2
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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { AboutComponent } from './about.component'
5import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
7import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' 3import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
4import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
8import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver' 5import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver'
6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
7import { AboutComponent } from './about.component'
9 8
10const aboutRoutes: Routes = [ 9const 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { AccountSearchComponent } from './account-search/account-search.component' 3import { AccountSearchComponent } from './account-search/account-search.component'
5import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' 4import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
6import { AccountVideosComponent } from './account-videos/account-videos.component' 5import { 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'
4import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes' 4import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes'
5import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes' 5import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes'
6import { SystemRoutes } from '@app/+admin/system' 6import { SystemRoutes } from '@app/+admin/system'
7import { MetaGuard } from '@ngx-meta/core'
8import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
9import { FollowsRoutes } from './follows' 8import { FollowsRoutes } from './follows'
10import { UsersRoutes } from './users' 9import { 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'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' 5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
6import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit' 6import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
7import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
8import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
7import { SharedFormModule } from '@app/shared/shared-forms' 9import { SharedFormModule } from '@app/shared/shared-forms'
8import { SharedGlobalIconModule } from '@app/shared/shared-icons' 10import { SharedGlobalIconModule } from '@app/shared/shared-icons'
9import { SharedMainModule } from '@app/shared/shared-main' 11import { SharedMainModule } from '@app/shared/shared-main'
10import { SharedModerationModule } from '@app/shared/shared-moderation' 12import { SharedModerationModule } from '@app/shared/shared-moderation'
11import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' 13import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
12import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
13import { AdminRoutingModule } from './admin-routing.module' 14import { AdminRoutingModule } from './admin-routing.module'
14import { AdminComponent } from './admin.component' 15import { AdminComponent } from './admin.component'
15import { 16import {
@@ -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
2import { pairwise } from 'rxjs/operators' 2import { pairwise } from 'rxjs/operators'
3import { Component, Input, OnInit } from '@angular/core' 3import { SelectOptionsItem } from 'src/types/select-options-item.model'
4import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
4import { FormGroup } from '@angular/forms' 5import { FormGroup } from '@angular/forms'
6import { MenuService } from '@app/core'
5import { ServerConfig } from '@shared/models' 7import { ServerConfig } from '@shared/models'
6import { ConfigService } from '../shared/config.service' 8import { 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})
13export class EditBasicConfigurationComponent implements OnInit { 15export 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
2import omit from 'lodash-es/omit'
2import { forkJoin } from 'rxjs' 3import { forkJoin } from 'rxjs'
3import { SelectOptionsItem } from 'src/types/select-options-item.model' 4import { SelectOptionsItem } from 'src/types/select-options-item.model'
4import { Component, OnInit } from '@angular/core' 5import { 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'
25import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' 26import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
26import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 27import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
27import { CustomConfig, ServerConfig } from '@shared/models' 28import { CustomPageService } from '@app/shared/shared-main/custom-page'
29import { CustomConfig, CustomPage, ServerConfig } from '@shared/models'
28import { EditConfigurationService } from './edit-configuration.service' 30import { EditConfigurationService } from './edit-configuration.service'
29 31
32type 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'
35export class EditCustomConfigComponent extends FormReactive implements OnInit { 41export 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 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { FormGroup } from '@angular/forms'
3import { 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})
10export 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'
2export * from './edit-basic-configuration.component' 2export * from './edit-basic-configuration.component'
3export * from './edit-configuration.service' 3export * from './edit-configuration.service'
4export * from './edit-custom-config.component' 4export * from './edit-custom-config.component'
5export * from './edit-homepage.component'
5export * from './edit-instance-information.component' 6export * from './edit-instance-information.component'
6export * from './edit-live-configuration.component' 7export * from './edit-live-configuration.component'
7export * from './edit-vod-transcoding.component' 8export * 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'
5import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core' 5import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
6import { PluginService } from '@app/core/plugins/plugin.service' 6import { PluginService } from '@app/core/plugins/plugin.service'
7import { compareSemVer } from '@shared/core-utils/miscs/miscs' 7import { compareSemVer } from '@shared/core-utils/miscs/miscs'
8import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' 8import { PeerTubePlugin, PluginType } from '@shared/models'
9import { 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'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' 5import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
6import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core' 6import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core'
7import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' 7import { PeerTubePluginIndex, PluginType } from '@shared/models'
8import { 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 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { HomeComponent } from './home.component'
5
6const 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})
18export 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
2import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
3import { CustomMarkupService } from '@app/shared/shared-custom-markup'
4import { CustomPageService } from '@app/shared/shared-main/custom-page'
5
6@Component({
7 templateUrl: './home.component.html',
8 styleUrls: [ './home.component.scss' ]
9})
10
11export 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 @@
1import { NgModule } from '@angular/core'
2import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
3import { SharedMainModule } from '@app/shared/shared-main'
4import { HomeRoutingModule } from './home-routing.module'
5import { 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})
25export 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 @@
1export * from './home-routing.module'
2export * from './home.component'
3export * 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { LoginComponent } from './login.component'
5import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' 3import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service'
4import { LoginComponent } from './login.component'
6 5
7const loginRoutes: Routes = [ 6const 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { LoginGuard } from '../core' 3import { LoginGuard } from '../core'
5import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' 4import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
5import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
6import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' 6import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
7import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' 7import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
8import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' 8import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
9import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 9import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
10import { MyAccountComponent } from './my-account.component' 10import { MyAccountComponent } from './my-account.component'
11import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
12 11
13const myAccountRoutes: Routes = [ 12const 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'
2import { HttpErrorResponse } from '@angular/common/http' 2import { HttpErrorResponse } from '@angular/common/http'
3import { AfterViewChecked, Component, OnInit } from '@angular/core' 3import { AfterViewChecked, Component, OnInit } from '@angular/core'
4import { AuthService, Notifier, User, UserService } from '@app/core' 4import { AuthService, Notifier, User, UserService } from '@app/core'
5import { uploadErrorHandler } from '@app/helpers' 5import { 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'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, Notifier, ServerService } from '@app/core' 5import { AuthService, Notifier, ServerService } from '@app/core'
6import { uploadErrorHandler } from '@app/helpers' 6import { genericUploadErrorHandler } from '@app/helpers'
7import { 7import {
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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { LoginGuard } from '../core' 3import { LoginGuard } from '../core'
5import { MyHistoryComponent } from './my-history/my-history.component' 4import { MyHistoryComponent } from './my-history/my-history.component'
6import { MyLibraryComponent } from './my-library.component' 5import { 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 @@
1import { switchMap } from 'rxjs/operators' 1import { SelectChannelItem } from 'src/types/select-options-item.model'
2import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
3import { AuthService, Notifier } from '@app/core' 3import { AuthService, Notifier } from '@app/core'
4import { listUserChannels } from '@app/helpers'
4import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' 5import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
6import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' 7import { VideoOwnershipService } from '@app/shared/shared-main'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 8import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { VideoChangeOwnership, VideoChannel } from '@shared/models' 9import { 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { ResetPasswordComponent } from './reset-password.component' 3import { ResetPasswordComponent } from './reset-password.component'
5 4
6const resetPasswordRoutes: Routes = [ 5const 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'
3import { AdvancedSearch } from '@app/shared/shared-search' 3import { AdvancedSearch } from '@app/shared/shared-search'
4import { ServerConfig, VideoConstant } from '@shared/models' 4import { ServerConfig, VideoConstant } from '@shared/models'
5 5
6type 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' 3import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
5import { SearchComponent } from './search.component' 4import { SearchComponent } from './search.component'
6import { VideoLazyLoadResolver } from './video-lazy-load.resolver' 5import { 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 @@
1import { forkJoin, of, Subscription } from 'rxjs' 1import { forkJoin, of, Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core' 2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ComponentPagination, HooksService, Notifier, ServerService, User, UserService } from '@app/core' 4import { AuthService, ComponentPagination, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
5import { immutableAssign } from '@app/helpers' 5import { immutableAssign } from '@app/helpers'
6import { Video, VideoChannel } from '@app/shared/shared-main' 6import { Video, VideoChannel } from '@app/shared/shared-main'
7import { AdvancedSearch, SearchService } from '@app/shared/shared-search' 7import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
8import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature' 8import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature'
9import { MetaService } from '@ngx-meta/core'
10import { SearchTargetType, ServerConfig } from '@shared/models' 9import { 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { ServerConfigResolver, UnloggedGuard } from '@app/core' 3import { ServerConfigResolver, UnloggedGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { RegisterComponent } from './register.component' 4import { RegisterComponent } from './register.component'
6 5
7const registerRoutes: Routes = [ 6const 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
5import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' 3import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component'
4import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
6 5
7const verifyAccountRoutes: Routes = [ 6const 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' 3import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
5import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 4import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
6import { VideoChannelsComponent } from './video-channels.component' 5import { 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 {
21import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' 21import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
22import { InstanceService } from '@app/shared/shared-instance' 22import { InstanceService } from '@app/shared/shared-instance'
23import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' 23import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
24import { LiveVideo, ServerConfig, VideoConstant, VideoDetails, VideoPrivacy } from '@shared/models' 24import {
25import { 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'
26import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' 33import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
27import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 34import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
28import { VideoEditType } from './video-edit.type' 35import { 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 @@
1import { objectToFormData } from '@app/helpers'
2import { 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 */
15export 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'
5import { FormValidatorService } from '@app/shared/shared-forms' 5import { FormValidatorService } from '@app/shared/shared-forms'
6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
7import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
8import { VideoPrivacy, VideoUpdate } from '@shared/models' 8import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
9import { hydrateFormFromVideo } from '../shared/video-edit-utils' 9import { hydrateFormFromVideo } from '../shared/video-edit-utils'
10import { VideoSend } from './video-send' 10import { 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 @@
1import { Subscription } from 'rxjs'
2import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'
3import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 1import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
4import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx'
4import { UploaderXFormData } from './uploaderx-form-data'
5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core'
6import { scrollToTop, uploadErrorHandler } from '@app/helpers' 6import { scrollToTop, genericUploadErrorHandler } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
11import { VideoPrivacy } from '@shared/models' 11import { VideoPrivacy } from '@shared/models'
12import { VideoSend } from './video-send' 12import { VideoSend } from './video-send'
13import { 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})
23export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate { 24export 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})`
289video 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 (
309video 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { CanDeactivateGuard, LoginGuard } from '@app/core' 3import { CanDeactivateGuard, LoginGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { VideoAddComponent } from './video-add.component' 4import { VideoAddComponent } from './video-add.component'
6 5
7const videoAddRoutes: Routes = [ 6const 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 @@
1import { Component, HostListener, OnInit, ViewChild } from '@angular/core' 1import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
2import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core' 3import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
3import { ServerConfig } from '@shared/models' 4import { ServerConfig } from '@shared/models'
4import { VideoEditType } from './shared/video-edit.type' 5import { 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { CanDeactivateGuard } from '@app/core' 2import { CanDeactivateGuard } from '@app/core'
3import { UploadxModule } from 'ngx-uploadx'
3import { VideoEditModule } from './shared/video-edit.module' 4import { VideoEditModule } from './shared/video-edit.module'
4import { DragDropDirective } from './video-add-components/drag-drop.directive' 5import { DragDropDirective } from './video-add-components/drag-drop.directive'
5import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' 6import { 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { CanDeactivateGuard, LoginGuard } from '@app/core' 3import { CanDeactivateGuard, LoginGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { VideoUpdateComponent } from './video-update.component' 4import { VideoUpdateComponent } from './video-update.component'
6import { VideoUpdateResolver } from './video-update.resolver' 5import { 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'
2import { map, switchMap } from 'rxjs/operators' 2import { map, switchMap } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { ActivatedRouteSnapshot, Resolve } from '@angular/router' 4import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
5import { VideoCaptionService, VideoChannelService, VideoDetails, VideoService } from '@app/shared/shared-main' 5import { AuthService } from '@app/core'
6import { listUserChannels } from '@app/helpers'
7import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
6import { LiveVideoService } from '@app/shared/shared-video-live' 8import { 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { VideoWatchComponent } from './video-watch.component' 3import { VideoWatchComponent } from './video-watch.component'
5 4
6const videoWatchRoutes: Routes = [ 5const 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'
25import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 26import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
26import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' 27import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature'
27import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 28import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
28import { MetaService } from '@ngx-meta/core'
29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
31import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' 31import { 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { LoginGuard } from '@app/core' 3import { LoginGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { VideoTrendingComponent } from './video-list' 4import { VideoTrendingComponent } from './video-list'
6import { VideoOverviewComponent } from './video-list/overview/video-overview.component' 5import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
7import { VideoLocalComponent } from './video-list/video-local.component' 6import { 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 }
3import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' 3import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
4import { MenuGuards } from '@app/core/routing/menu-guard.service' 4import { MenuGuards } from '@app/core/routing/menu-guard.service'
5import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' 5import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
6import { PreloadSelectedModulesList } from './core' 6import { MetaGuard, PreloadSelectedModulesList } from './core'
7import { EmptyComponent } from './empty.component' 7import { EmptyComponent } from './empty.component'
8import { RootComponent } from './root.component' 8import { 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'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { BrowserModule } from '@angular/platform-browser' 5import { BrowserModule } from '@angular/platform-browser'
6import { ServiceWorkerModule } from '@angular/service-worker' 6import { ServiceWorkerModule } from '@angular/service-worker'
7import { ServerService } from '@app/core'
8import localeOc from '@app/helpers/locales/oc' 7import localeOc from '@app/helpers/locales/oc'
9import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
10import { AppRoutingModule } from './app-routing.module' 8import { AppRoutingModule } from './app-routing.module'
11import { AppComponent } from './app.component' 9import { AppComponent } from './app.component'
12import { CoreModule } from './core' 10import { CoreModule } from './core'
@@ -19,12 +17,12 @@ import { CustomModalComponent } from './modal/custom-modal.component'
19import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' 17import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component'
20import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' 18import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component'
21import { WelcomeModalComponent } from './modal/welcome-modal.component' 19import { WelcomeModalComponent } from './modal/welcome-modal.component'
20import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
22import { SharedFormModule } from './shared/shared-forms' 21import { SharedFormModule } from './shared/shared-forms'
23import { SharedGlobalIconModule } from './shared/shared-icons' 22import { SharedGlobalIconModule } from './shared/shared-icons'
24import { SharedInstanceModule } from './shared/shared-instance' 23import { SharedInstanceModule } from './shared/shared-instance'
25import { SharedMainModule } from './shared/shared-main' 24import { SharedMainModule } from './shared/shared-main'
26import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings' 25import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings'
27import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
28 26
29registerLocaleData(localeOc, 'oc') 27registerLocaleData(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'
14import { Notifier } from './notification' 14import { Notifier } from './notification'
15import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer' 15import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
16import { RestExtractor, RestService } from './rest' 16import { RestExtractor, RestService } from './rest'
17import { LoginGuard, RedirectService, UnloggedGuard, UserRightGuard } from './routing' 17import { LoginGuard, MetaGuard, MetaService, RedirectService, UnloggedGuard, UserRightGuard } from './routing'
18import { CanDeactivateGuard } from './routing/can-deactivate-guard.service' 18import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
19import { ServerConfigResolver } from './routing/server-config-resolver.service' 19import { ServerConfigResolver } from './routing/server-config-resolver.service'
20import { ScopedTokensService } from './scoped-tokens' 20import { 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})
83export class CoreModule { 86export 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 @@
1import { fromEvent } from 'rxjs' 1import { fromEvent } from 'rxjs'
2import { debounceTime } from 'rxjs/operators' 2import { debounceTime } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { GlobalIconName } from '@app/shared/shared-icons'
5import { sortObjectComparator } from '@shared/core-utils/miscs/miscs'
6import { ServerConfig } from '@shared/models/server'
4import { ScreenService } from '../wrappers' 7import { ScreenService } from '../wrappers'
5 8
9export type MenuLink = {
10 icon: GlobalIconName
11 label: string
12 menuLabel: string
13 path: string
14 priority: number
15}
16
6@Injectable() 17@Injectable()
7export class MenuService { 18export 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 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { LinkifierService } from './linkifier.service' 2import { LinkifierService } from './linkifier.service'
3import { SANITIZE_OPTIONS } from '@shared/core-utils/renderer/html' 3import { getCustomMarkupSanitizeOptions, getSanitizeOptions } from '@shared/core-utils/renderer/html'
4 4
5@Injectable() 5@Injectable()
6export class HtmlRendererService { 6export 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
23type MarkdownConfig = { 25type 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'
3export * from './disable-for-reuse-hook' 3export * from './disable-for-reuse-hook'
4export * from './login-guard.service' 4export * from './login-guard.service'
5export * from './menu-guard.service' 5export * from './menu-guard.service'
6export * from './meta-guard.service'
7export * from './meta.service'
6export * from './preload-selected-modules-list' 8export * from './preload-selected-modules-list'
7export * from './redirect.service' 9export * from './redirect.service'
8export * from './server-config-resolver.service' 10export * 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 @@
1import { Injectable } from '@angular/core'
2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'
3import { MetaService } from './meta.service'
4
5@Injectable()
6export 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 @@
1import { Injectable } from '@angular/core'
2import { Meta, Title } from '@angular/platform-browser'
3import { HTMLServerConfig } from '@shared/models/server'
4import { ServerService } from '../server'
5
6export interface MetaSettings {
7 title?: string
8}
9
10@Injectable()
11export 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'
6export class RedirectService { 6export 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'
3import { HttpClient } from '@angular/common/http' 3import { HttpClient } from '@angular/common/http'
4import { Inject, Injectable, LOCALE_ID } from '@angular/core' 4import { Inject, Injectable, LOCALE_ID } from '@angular/core'
5import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers' 5import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers'
6import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
7import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' 6import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
8import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models' 7import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models'
9import { environment } from '../../../environments/environment' 8import { 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 @@
1import { map } from 'rxjs/operators' 1import { first, map } from 'rxjs/operators'
2import { SelectChannelItem } from 'src/types/select-options-item.model' 2import { SelectChannelItem } from 'src/types/select-options-item.model'
3import { DatePipe } from '@angular/common' 3import { DatePipe } from '@angular/common'
4import { HttpErrorResponse } from '@angular/common/http' 4import { HttpErrorResponse } from '@angular/common/http'
@@ -23,20 +23,29 @@ function getParameterByName (name: string, url: string) {
23 23
24function listUserChannels (authService: AuthService) { 24function 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
42function getAbsoluteAPIUrl () { 51function getAbsoluteAPIUrl () {
@@ -167,8 +176,8 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) {
167 ) 176 )
168} 177}
169 178
170function uploadErrorHandler (parameters: { 179function 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'
4import { ViewportScroller } from '@angular/common' 4import { ViewportScroller } from '@angular/common'
5import { Component, OnInit, ViewChild } from '@angular/core' 5import { Component, OnInit, ViewChild } from '@angular/core'
6import { Router } from '@angular/router' 6import { Router } from '@angular/router'
7import { AuthService, AuthStatus, AuthUser, MenuService, RedirectService, ScreenService, ServerService, UserService } from '@app/core' 7import {
8 AuthService,
9 AuthStatus,
10 AuthUser,
11 MenuLink,
12 MenuService,
13 RedirectService,
14 ScreenService,
15 ServerService,
16 UserService
17} from '@app/core'
8import { scrollToTop } from '@app/helpers' 18import { scrollToTop } from '@app/helpers'
9import { LanguageChooserComponent } from '@app/menu/language-chooser.component' 19import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
10import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' 20import { 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 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { 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})
13export 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 @@
1import { ComponentRef, Injectable } from '@angular/core'
2import { MarkdownService } from '@app/core'
3import {
4 ChannelMiniatureMarkupData,
5 EmbedMarkupData,
6 PlaylistMiniatureMarkupData,
7 VideoMiniatureMarkupData,
8 VideosListMarkupData
9} from '@shared/models'
10import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
11import { DynamicElementService } from './dynamic-element.service'
12import { EmbedMarkupComponent } from './embed-markup.component'
13import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
14import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
15import { VideosListMarkupComponent } from './videos-list-markup.component'
16
17type BuilderFunction = (el: HTMLElement) => ComponentRef<any>
18
19@Injectable()
20export 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 @@
1import {
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()
15export 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 @@
1import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
2import { environment } from 'src/environments/environment'
3import { Component, ElementRef, Input, OnInit } from '@angular/core'
4
5@Component({
6 selector: 'my-embed-markup',
7 template: ''
8})
9export 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 @@
1export * from './custom-markup.service'
2export * from './dynamic-element.service'
3export * 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
4my-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 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { MiniatureDisplayOptions } from '../shared-video-miniature'
3import { 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})
14export 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
2import { CommonModule } from '@angular/common'
3import { NgModule } from '@angular/core'
4import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
5import { SharedGlobalIconModule } from '../shared-icons'
6import { SharedMainModule } from '../shared-main'
7import { SharedVideoMiniatureModule } from '../shared-video-miniature'
8import { SharedVideoPlaylistModule } from '../shared-video-playlist'
9import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
10import { CustomMarkupService } from './custom-markup.service'
11import { DynamicElementService } from './dynamic-element.service'
12import { EmbedMarkupComponent } from './embed-markup.component'
13import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
14import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
15import { 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})
49export 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
4my-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 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { AuthService } from '@app/core'
3import { Video, VideoService } from '../shared-main'
4import { 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})
15export 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
4my-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 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { AuthService } from '@app/core'
3import { VideoSortField } from '@shared/models'
4import { Video, VideoService } from '../shared-main'
5import { 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})
16export 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 @@
1import { ViewportScroller } from '@angular/common'
2import truncate from 'lodash-es/truncate' 1import truncate from 'lodash-es/truncate'
3import { Subject } from 'rxjs' 2import { Subject } from 'rxjs'
4import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 3import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
4import { ViewportScroller } from '@angular/common'
5import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core' 5import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'
6import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 6import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
7import { SafeHtml } from '@angular/platform-browser'
7import { MarkdownService, ScreenService } from '@app/core' 8import { MarkdownService, ScreenService } from '@app/core'
8 9
9@Component({ 10@Component({
@@ -21,18 +22,27 @@ import { MarkdownService, ScreenService } from '@app/core'
21 22
22export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { 23export 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'
4export class Account extends Actor implements ServerAccount { 4export 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' })
5export class FromNowPipe implements PipeTransform { 5export 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 @@
1import { of } from 'rxjs'
2import { catchError, map } from 'rxjs/operators'
3import { HttpClient } from '@angular/common/http'
4import { Injectable } from '@angular/core'
5import { RestExtractor } from '@app/core'
6import { CustomPage } from '@shared/models'
7import { environment } from '../../../../environments/environment'
8
9@Injectable()
10export 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
9export class PluginPlaceholderComponent { 10export 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'
30import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 30import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
31import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' 31import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons'
32import { CustomPageService } from './custom-page'
32import { DateToggleComponent } from './date' 33import { DateToggleComponent } from './date'
33import { FeedComponent } from './feeds' 34import { FeedComponent } from './feeds'
34import { LoaderComponent, SmallLoaderComponent } from './loaders' 35import { 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})
178export class SharedMainModule { } 181export 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 @@
1import { BooleanBothQuery, SearchTargetType } from '@shared/models' 1import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models'
2 2
3export class AdvancedSearch { 3export 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