diff options
Diffstat (limited to 'client')
91 files changed, 1297 insertions, 555 deletions
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index c3ef1d894..43dbbebb3 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html | |||
@@ -1,11 +1,21 @@ | |||
1 | <div *ngIf="account" class="row"> | 1 | <div class="margin-content"> |
2 | <a | 2 | |
3 | *ngFor="let videoChannel of videoChannels" [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" | 3 | <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div> |
4 | class="video-channel" i18n-title title="See this video channel" | 4 | |
5 | > | 5 | <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"> |
6 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | 6 | <div class="section channel" *ngFor="let videoChannel of videoChannels"> |
7 | 7 | <div class="section-title"> | |
8 | <div class="video-channel-display-name">{{ videoChannel.displayName }}</div> | 8 | <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" i18n-title title="See this video channel"> |
9 | <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div> | 9 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> |
10 | </a> | 10 | |
11 | </div> \ No newline at end of file | 11 | <div>{{ videoChannel.displayName }}</div> |
12 | <div i18n class="followers">{{ videoChannel.followersCount }} subscribers</div> | ||
13 | </a> | ||
14 | |||
15 | <my-subscribe-button [videoChannel]="videoChannel"></my-subscribe-button> | ||
16 | </div> | ||
17 | |||
18 | <my-video-miniature *ngFor="let video of getVideosOf(videoChannel)" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature> | ||
19 | </div> | ||
20 | </div> | ||
21 | </div> | ||
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 0c6de2efa..d9f78bdcd 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 | |||
@@ -1,30 +1,17 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_miniature'; | ||
3 | 4 | ||
4 | .row { | 5 | .margin-content { |
5 | justify-content: center; | 6 | @include adapt-margin-content-width; |
6 | } | 7 | } |
7 | 8 | ||
8 | a.video-channel { | 9 | .section { |
9 | @include disable-default-a-behaviour; | 10 | @include miniature-rows; |
10 | 11 | ||
11 | display: inline-block; | 12 | padding-top: 0 !important; |
12 | text-align: center; | ||
13 | color: var(--mainForegroundColor); | ||
14 | margin: 10px 30px; | ||
15 | 13 | ||
16 | img { | 14 | .section-title { |
17 | @include avatar(80px); | 15 | align-items: center; |
18 | |||
19 | margin-bottom: 10px; | ||
20 | } | ||
21 | |||
22 | .video-channel-display-name { | ||
23 | font-size: 20px; | ||
24 | font-weight: $font-bold; | ||
25 | } | 16 | } |
26 | 17 | } | |
27 | .video-channel-followers { | ||
28 | font-size: 15px; | ||
29 | } | ||
30 | } \ No newline at end of file | ||
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 44f5626bb..ee3b5f8e4 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 | |||
@@ -3,9 +3,14 @@ import { ActivatedRoute } from '@angular/router' | |||
3 | import { Account } from '@app/shared/account/account.model' | 3 | import { Account } from '@app/shared/account/account.model' |
4 | import { AccountService } from '@app/shared/account/account.service' | 4 | import { AccountService } from '@app/shared/account/account.service' |
5 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 5 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
6 | import { flatMap, map, tap } from 'rxjs/operators' | 6 | import { concatMap, map, switchMap, tap } from 'rxjs/operators' |
7 | import { Subscription } from 'rxjs' | 7 | import { from, Subscription } from 'rxjs' |
8 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 8 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
9 | import { Video } from '@app/shared/video/video.model' | ||
10 | import { AuthService } from '@app/core' | ||
11 | import { VideoService } from '@app/shared/video/video.service' | ||
12 | import { VideoSortField } from '@app/shared/video/sort-field.type' | ||
13 | import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' | ||
9 | 14 | ||
10 | @Component({ | 15 | @Component({ |
11 | selector: 'my-account-video-channels', | 16 | selector: 'my-account-video-channels', |
@@ -15,27 +20,73 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | |||
15 | export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | 20 | export class AccountVideoChannelsComponent implements OnInit, OnDestroy { |
16 | account: Account | 21 | account: Account |
17 | videoChannels: VideoChannel[] = [] | 22 | videoChannels: VideoChannel[] = [] |
23 | videos: { [id: number]: Video[] } = {} | ||
24 | |||
25 | channelPagination: ComponentPagination = { | ||
26 | currentPage: 1, | ||
27 | itemsPerPage: 2 | ||
28 | } | ||
29 | |||
30 | videosPagination: ComponentPagination = { | ||
31 | currentPage: 1, | ||
32 | itemsPerPage: 12 | ||
33 | } | ||
34 | videosSort: VideoSortField = '-publishedAt' | ||
18 | 35 | ||
19 | private accountSub: Subscription | 36 | private accountSub: Subscription |
20 | 37 | ||
21 | constructor ( | 38 | constructor ( |
22 | protected route: ActivatedRoute, | 39 | private route: ActivatedRoute, |
40 | private authService: AuthService, | ||
23 | private accountService: AccountService, | 41 | private accountService: AccountService, |
24 | private videoChannelService: VideoChannelService | 42 | private videoChannelService: VideoChannelService, |
43 | private videoService: VideoService | ||
25 | ) { } | 44 | ) { } |
26 | 45 | ||
46 | get user () { | ||
47 | return this.authService.getUser() | ||
48 | } | ||
49 | |||
27 | ngOnInit () { | 50 | ngOnInit () { |
28 | // Parent get the account for us | 51 | // Parent get the account for us |
29 | this.accountSub = this.accountService.accountLoaded | 52 | this.accountSub = this.accountService.accountLoaded |
30 | .pipe( | 53 | .subscribe(account => { |
31 | tap(account => this.account = account), | 54 | this.account = account |
32 | flatMap(account => this.videoChannelService.listAccountVideoChannels(account)), | 55 | |
33 | map(res => res.data) | 56 | this.loadMoreChannels() |
34 | ) | 57 | }) |
35 | .subscribe(videoChannels => this.videoChannels = videoChannels) | ||
36 | } | 58 | } |
37 | 59 | ||
38 | ngOnDestroy () { | 60 | ngOnDestroy () { |
39 | if (this.accountSub) this.accountSub.unsubscribe() | 61 | if (this.accountSub) this.accountSub.unsubscribe() |
40 | } | 62 | } |
63 | |||
64 | loadMoreChannels () { | ||
65 | this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination) | ||
66 | .pipe( | ||
67 | tap(res => this.channelPagination.totalItems = res.total), | ||
68 | switchMap(res => from(res.data)), | ||
69 | concatMap(videoChannel => { | ||
70 | return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort) | ||
71 | .pipe(map(data => ({ videoChannel, videos: data.videos }))) | ||
72 | }) | ||
73 | ) | ||
74 | .subscribe(({ videoChannel, videos }) => { | ||
75 | this.videoChannels.push(videoChannel) | ||
76 | |||
77 | this.videos[videoChannel.id] = videos | ||
78 | }) | ||
79 | } | ||
80 | |||
81 | getVideosOf (videoChannel: VideoChannel) { | ||
82 | return this.videos[ videoChannel.id ] || [] | ||
83 | } | ||
84 | |||
85 | onNearOfBottom () { | ||
86 | if (!hasMoreItems(this.channelPagination)) return | ||
87 | |||
88 | this.channelPagination.currentPage += 1 | ||
89 | |||
90 | this.loadMoreChannels() | ||
91 | } | ||
41 | } | 92 | } |
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index 0d579fa0c..5a99aadce 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts | |||
@@ -29,6 +29,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
29 | private accountSub: Subscription | 29 | private accountSub: Subscription |
30 | 30 | ||
31 | constructor ( | 31 | constructor ( |
32 | protected i18n: I18n, | ||
32 | protected router: Router, | 33 | protected router: Router, |
33 | protected serverService: ServerService, | 34 | protected serverService: ServerService, |
34 | protected route: ActivatedRoute, | 35 | protected route: ActivatedRoute, |
@@ -36,13 +37,10 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
36 | protected notifier: Notifier, | 37 | protected notifier: Notifier, |
37 | protected confirmService: ConfirmService, | 38 | protected confirmService: ConfirmService, |
38 | protected screenService: ScreenService, | 39 | protected screenService: ScreenService, |
39 | private i18n: I18n, | ||
40 | private accountService: AccountService, | 40 | private accountService: AccountService, |
41 | private videoService: VideoService | 41 | private videoService: VideoService |
42 | ) { | 42 | ) { |
43 | super() | 43 | super() |
44 | |||
45 | this.titlePage = this.i18n('Published videos') | ||
46 | } | 44 | } |
47 | 45 | ||
48 | ngOnInit () { | 46 | ngOnInit () { |
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts index 531d763c4..55bce351a 100644 --- a/client/src/app/+accounts/accounts-routing.module.ts +++ b/client/src/app/+accounts/accounts-routing.module.ts | |||
@@ -14,7 +14,7 @@ const accountsRoutes: Routes = [ | |||
14 | children: [ | 14 | children: [ |
15 | { | 15 | { |
16 | path: '', | 16 | path: '', |
17 | redirectTo: 'videos', | 17 | redirectTo: 'video-channels', |
18 | pathMatch: 'full' | 18 | pathMatch: 'full' |
19 | }, | 19 | }, |
20 | { | 20 | { |
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index c1377c1ea..038e18c4b 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -26,10 +26,10 @@ | |||
26 | </div> | 26 | </div> |
27 | 27 | ||
28 | <div class="links"> | 28 | <div class="links"> |
29 | <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a> | ||
30 | |||
31 | <a i18n routerLink="video-channels" routerLinkActive="active" class="title-page">Video channels</a> | 29 | <a i18n routerLink="video-channels" routerLinkActive="active" class="title-page">Video channels</a> |
32 | 30 | ||
31 | <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a> | ||
32 | |||
33 | <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a> | 33 | <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a> |
34 | </div> | 34 | </div> |
35 | </div> | 35 | </div> |
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 637484622..44fc6dc26 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 | |||
@@ -287,6 +287,14 @@ | |||
287 | </div> | 287 | </div> |
288 | 288 | ||
289 | <div class="form-group"> | 289 | <div class="form-group"> |
290 | <my-peertube-checkbox | ||
291 | inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles" | ||
292 | i18n-labelText labelText="Allow audio files upload" | ||
293 | i18n-helpHtml helpHtml="Allow your users to upload audio files that will be merged with the preview file on upload" | ||
294 | ></my-peertube-checkbox> | ||
295 | </div> | ||
296 | |||
297 | <div class="form-group"> | ||
290 | <label i18n for="transcodingThreads">Transcoding threads</label> | 298 | <label i18n for="transcodingThreads">Transcoding threads</label> |
291 | <div class="peertube-select-container"> | 299 | <div class="peertube-select-container"> |
292 | <select id="transcodingThreads" formControlName="threads"> | 300 | <select id="transcodingThreads" formControlName="threads"> |
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 e64750713..c238a6c81 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 | |||
@@ -116,6 +116,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
116 | enabled: null, | 116 | enabled: null, |
117 | threads: this.customConfigValidatorsService.TRANSCODING_THREADS, | 117 | threads: this.customConfigValidatorsService.TRANSCODING_THREADS, |
118 | allowAdditionalExtensions: null, | 118 | allowAdditionalExtensions: null, |
119 | allowAudioFiles: null, | ||
119 | resolutions: {} | 120 | resolutions: {} |
120 | }, | 121 | }, |
121 | autoBlacklist: { | 122 | autoBlacklist: { |
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts index 73340d21a..13607119e 100644 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.ts +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.ts | |||
@@ -27,6 +27,7 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn | |||
27 | videosHistoryEnabled: boolean | 27 | videosHistoryEnabled: boolean |
28 | 28 | ||
29 | constructor ( | 29 | constructor ( |
30 | protected i18n: I18n, | ||
30 | protected router: Router, | 31 | protected router: Router, |
31 | protected serverService: ServerService, | 32 | protected serverService: ServerService, |
32 | protected route: ActivatedRoute, | 33 | protected route: ActivatedRoute, |
@@ -34,7 +35,6 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn | |||
34 | protected userService: UserService, | 35 | protected userService: UserService, |
35 | protected notifier: Notifier, | 36 | protected notifier: Notifier, |
36 | protected screenService: ScreenService, | 37 | protected screenService: ScreenService, |
37 | protected i18n: I18n, | ||
38 | private confirmService: ConfirmService, | 38 | private confirmService: ConfirmService, |
39 | private videoService: VideoService, | 39 | private videoService: VideoService, |
40 | private userHistoryService: UserHistoryService | 40 | private userHistoryService: UserHistoryService |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html index 81fb11f45..f87df87df 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html | |||
@@ -61,5 +61,12 @@ When you will upload a video in this channel, the video support field will be au | |||
61 | </div> | 61 | </div> |
62 | </div> | 62 | </div> |
63 | 63 | ||
64 | <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()"> | ||
65 | <my-peertube-checkbox | ||
66 | inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate" | ||
67 | i18n-labelText labelText="Overwrite support field of all videos of this channel" | ||
68 | ></my-peertube-checkbox> | ||
69 | </div> | ||
70 | |||
64 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | 71 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> |
65 | </form> | 72 | </form> |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts index 4dc65dd99..7479442d1 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts | |||
@@ -11,4 +11,9 @@ export abstract class MyAccountVideoChannelEdit extends FormReactive { | |||
11 | 11 | ||
12 | // FIXME: We need this method so angular does not complain in the child template | 12 | // FIXME: We need this method so angular does not complain in the child template |
13 | onAvatarChange (formData: FormData) { /* empty */ } | 13 | onAvatarChange (formData: FormData) { /* empty */ } |
14 | |||
15 | // Should be implemented by the child | ||
16 | isBulkUpdateVideosDisplayed () { | ||
17 | return false | ||
18 | } | ||
14 | } | 19 | } |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts index da4fb645a..081e956d2 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts | |||
@@ -20,6 +20,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
20 | videoChannelToUpdate: VideoChannel | 20 | videoChannelToUpdate: VideoChannel |
21 | 21 | ||
22 | private paramsSub: Subscription | 22 | private paramsSub: Subscription |
23 | private oldSupportField: string | ||
23 | 24 | ||
24 | constructor ( | 25 | constructor ( |
25 | protected formValidatorService: FormValidatorService, | 26 | protected formValidatorService: FormValidatorService, |
@@ -39,7 +40,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
39 | this.buildForm({ | 40 | this.buildForm({ |
40 | 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, | 41 | 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, |
41 | description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION, | 42 | description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION, |
42 | support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT | 43 | support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT, |
44 | bulkVideosSupportUpdate: null | ||
43 | }) | 45 | }) |
44 | 46 | ||
45 | this.paramsSub = this.route.params.subscribe(routeParams => { | 47 | this.paramsSub = this.route.params.subscribe(routeParams => { |
@@ -49,6 +51,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
49 | videoChannelToUpdate => { | 51 | videoChannelToUpdate => { |
50 | this.videoChannelToUpdate = videoChannelToUpdate | 52 | this.videoChannelToUpdate = videoChannelToUpdate |
51 | 53 | ||
54 | this.oldSupportField = videoChannelToUpdate.support | ||
55 | |||
52 | this.form.patchValue({ | 56 | this.form.patchValue({ |
53 | 'display-name': videoChannelToUpdate.displayName, | 57 | 'display-name': videoChannelToUpdate.displayName, |
54 | description: videoChannelToUpdate.description, | 58 | description: videoChannelToUpdate.description, |
@@ -72,7 +76,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
72 | const videoChannelUpdate: VideoChannelUpdate = { | 76 | const videoChannelUpdate: VideoChannelUpdate = { |
73 | displayName: body['display-name'], | 77 | displayName: body['display-name'], |
74 | description: body.description || null, | 78 | description: body.description || null, |
75 | support: body.support || null | 79 | support: body.support || null, |
80 | bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false | ||
76 | } | 81 | } |
77 | 82 | ||
78 | this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( | 83 | this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( |
@@ -118,4 +123,10 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
118 | getFormButtonTitle () { | 123 | getFormButtonTitle () { |
119 | return this.i18n('Update') | 124 | return this.i18n('Update') |
120 | } | 125 | } |
126 | |||
127 | isBulkUpdateVideosDisplayed () { | ||
128 | if (this.oldSupportField === undefined) return false | ||
129 | |||
130 | return this.oldSupportField !== this.form.value['support'] | ||
131 | } | ||
121 | } | 132 | } |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html index 303fc46f7..82321459f 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html | |||
@@ -57,10 +57,12 @@ | |||
57 | </div> | 57 | </div> |
58 | 58 | ||
59 | <div class="form-group"> | 59 | <div class="form-group"> |
60 | <my-image-upload | 60 | <label i18n>Playlist thumbnail</label> |
61 | i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile" | 61 | |
62 | previewWidth="200px" previewHeight="110px" | 62 | <my-preview-upload |
63 | ></my-image-upload> | 63 | i18n-inputLabel inputLabel="Edit" inputName="thumbnailfile" formControlName="thumbnailfile" |
64 | previewWidth="223px" previewHeight="122px" | ||
65 | ></my-preview-upload> | ||
64 | </div> | 66 | </div> |
65 | </div> | 67 | </div> |
66 | </div> | 68 | </div> |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts index fbfb4c8f7..81dd9a75f 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts | |||
@@ -1,6 +1,4 @@ | |||
1 | import { FormReactive } from '@app/shared' | 1 | import { FormReactive } from '@app/shared' |
2 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | ||
3 | import { ServerService } from '@app/core' | ||
4 | import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' | 2 | import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' |
5 | 3 | ||
6 | export abstract class MyAccountVideoPlaylistEdit extends FormReactive { | 4 | export abstract class MyAccountVideoPlaylistEdit extends FormReactive { |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index 84d464800..2854093c4 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html | |||
@@ -20,7 +20,7 @@ | |||
20 | <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button> | 20 | <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button> |
21 | 21 | ||
22 | <my-button i18n-label label="Change ownership" | 22 | <my-button i18n-label label="Change ownership" |
23 | className="action-button-change-ownership" | 23 | className="action-button-change-ownership grey-button" |
24 | icon="im-with-her" | 24 | icon="im-with-her" |
25 | (click)="changeOwnership($event, video)" | 25 | (click)="changeOwnership($event, video)" |
26 | ></my-button> | 26 | ></my-button> |
diff --git a/client/src/app/+signup/+register/custom-stepper.component.html b/client/src/app/+signup/+register/custom-stepper.component.html new file mode 100644 index 000000000..bf507fc4f --- /dev/null +++ b/client/src/app/+signup/+register/custom-stepper.component.html | |||
@@ -0,0 +1,25 @@ | |||
1 | <section class="container"> | ||
2 | <header> | ||
3 | <ng-container *ngFor="let step of steps; let i = index; let isLast = last;"> | ||
4 | <div | ||
5 | class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step) }" | ||
6 | (click)="onClick(i)" | ||
7 | > | ||
8 | <div class="step-index"> | ||
9 | <ng-container *ngIf="!isCompleted(step)">{{ i + 1 }}</ng-container> | ||
10 | <my-global-icon *ngIf="isCompleted(step)" iconName="tick"></my-global-icon> | ||
11 | </div> | ||
12 | |||
13 | <div class="step-label">{{ step.label }}</div> | ||
14 | </div> | ||
15 | |||
16 | <!-- Do no display if this is the last child --> | ||
17 | <div *ngIf="!isLast" class="connector"></div> | ||
18 | </ng-container> | ||
19 | </header> | ||
20 | |||
21 | <div [style.display]="selected ? 'block' : 'none'"> | ||
22 | <ng-container [ngTemplateOutlet]="selected.content"></ng-container> | ||
23 | </div> | ||
24 | |||
25 | </section> | ||
diff --git a/client/src/app/+signup/+register/custom-stepper.component.scss b/client/src/app/+signup/+register/custom-stepper.component.scss new file mode 100644 index 000000000..2371c8ae5 --- /dev/null +++ b/client/src/app/+signup/+register/custom-stepper.component.scss | |||
@@ -0,0 +1,66 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | $grey-color: #9CA3AB; | ||
5 | $index-block-height: 32px; | ||
6 | |||
7 | header { | ||
8 | display: flex; | ||
9 | justify-content: space-between; | ||
10 | font-size: 15px; | ||
11 | margin-bottom: 30px; | ||
12 | |||
13 | .step-info { | ||
14 | color: $grey-color; | ||
15 | display: flex; | ||
16 | flex-direction: column; | ||
17 | align-items: center; | ||
18 | width: $index-block-height; | ||
19 | |||
20 | .step-index { | ||
21 | display: flex; | ||
22 | justify-content: center; | ||
23 | align-items: center; | ||
24 | width: $index-block-height; | ||
25 | height: $index-block-height; | ||
26 | border-radius: 100px; | ||
27 | border: 2px solid $grey-color; | ||
28 | margin-bottom: 10px; | ||
29 | |||
30 | my-global-icon { | ||
31 | @include apply-svg-color(var(--mainBackgroundColor)); | ||
32 | |||
33 | width: 22px; | ||
34 | height: 22px; | ||
35 | } | ||
36 | } | ||
37 | |||
38 | .step-label { | ||
39 | width: max-content; | ||
40 | } | ||
41 | |||
42 | &.active, | ||
43 | &.completed { | ||
44 | .step-index { | ||
45 | border-color: var(--mainColor); | ||
46 | background-color: var(--mainColor); | ||
47 | color: var(--mainBackgroundColor); | ||
48 | } | ||
49 | |||
50 | .step-label { | ||
51 | color: var(--mainColor); | ||
52 | } | ||
53 | } | ||
54 | |||
55 | &.completed { | ||
56 | cursor: pointer; | ||
57 | } | ||
58 | } | ||
59 | |||
60 | .connector { | ||
61 | flex: auto; | ||
62 | margin: $index-block-height/2 10px 0 10px; | ||
63 | height: 2px; | ||
64 | background-color: $grey-color; | ||
65 | } | ||
66 | } | ||
diff --git a/client/src/app/+signup/+register/custom-stepper.component.ts b/client/src/app/+signup/+register/custom-stepper.component.ts new file mode 100644 index 000000000..2ae40f3a9 --- /dev/null +++ b/client/src/app/+signup/+register/custom-stepper.component.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | import { CdkStep, CdkStepper } from '@angular/cdk/stepper' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-custom-stepper', | ||
6 | templateUrl: './custom-stepper.component.html', | ||
7 | styleUrls: [ './custom-stepper.component.scss' ], | ||
8 | providers: [ { provide: CdkStepper, useExisting: CustomStepperComponent } ] | ||
9 | }) | ||
10 | export class CustomStepperComponent extends CdkStepper { | ||
11 | |||
12 | onClick (index: number): void { | ||
13 | this.selectedIndex = index | ||
14 | } | ||
15 | |||
16 | isCompleted (step: CdkStep) { | ||
17 | return step.stepControl && step.stepControl.dirty && step.stepControl.valid | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/signup/signup-routing.module.ts b/client/src/app/+signup/+register/register-routing.module.ts index 820d16d4d..e3a5001dc 100644 --- a/client/src/app/signup/signup-routing.module.ts +++ b/client/src/app/+signup/+register/register-routing.module.ts | |||
@@ -1,17 +1,18 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
4 | import { SignupComponent } from './signup.component' | 4 | import { RegisterComponent } from './register.component' |
5 | import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' | 5 | import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' |
6 | import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' | ||
6 | 7 | ||
7 | const signupRoutes: Routes = [ | 8 | const registerRoutes: Routes = [ |
8 | { | 9 | { |
9 | path: 'signup', | 10 | path: '', |
10 | component: SignupComponent, | 11 | component: RegisterComponent, |
11 | canActivate: [ MetaGuard ], | 12 | canActivate: [ MetaGuard, UnloggedGuard ], |
12 | data: { | 13 | data: { |
13 | meta: { | 14 | meta: { |
14 | title: 'Signup' | 15 | title: 'Register' |
15 | } | 16 | } |
16 | }, | 17 | }, |
17 | resolve: { | 18 | resolve: { |
@@ -21,7 +22,7 @@ const signupRoutes: Routes = [ | |||
21 | ] | 22 | ] |
22 | 23 | ||
23 | @NgModule({ | 24 | @NgModule({ |
24 | imports: [ RouterModule.forChild(signupRoutes) ], | 25 | imports: [ RouterModule.forChild(registerRoutes) ], |
25 | exports: [ RouterModule ] | 26 | exports: [ RouterModule ] |
26 | }) | 27 | }) |
27 | export class SignupRoutingModule {} | 28 | export class RegisterRoutingModule {} |
diff --git a/client/src/app/+signup/+register/register-step-channel.component.html b/client/src/app/+signup/+register/register-step-channel.component.html new file mode 100644 index 000000000..68ea4473a --- /dev/null +++ b/client/src/app/+signup/+register/register-step-channel.component.html | |||
@@ -0,0 +1,50 @@ | |||
1 | <form role="form" [formGroup]="form"> | ||
2 | |||
3 | <div class="channel-explanations"> | ||
4 | <p i18n> | ||
5 | A channel is an entity in which you upload your videos. Creating several of them helps you to organize and separate your content.<br /> | ||
6 | For example, you could decide to have a channel to publish your piano concerts, and another channel in which you publish your videos talking about ecology. | ||
7 | </p> | ||
8 | |||
9 | <p> | ||
10 | Other users can decide to subscribe any channel they want, to be notified when you publish a new video. | ||
11 | </p> | ||
12 | </div> | ||
13 | |||
14 | <div class="form-group"> | ||
15 | <label for="name" i18n>Channel name</label> | ||
16 | |||
17 | <div class="input-group"> | ||
18 | <input | ||
19 | type="text" id="name" i18n-placeholder placeholder="Example: my_super_channel" | ||
20 | formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" | ||
21 | > | ||
22 | <div class="input-group-append"> | ||
23 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
24 | </div> | ||
25 | </div> | ||
26 | |||
27 | <div *ngIf="formErrors.name" class="form-error"> | ||
28 | {{ formErrors.name }} | ||
29 | </div> | ||
30 | |||
31 | <div *ngIf="isSameThanUsername()" class="form-error" i18n> | ||
32 | Channel name cannot be the same than your account name. You can click on the first step to update your account name. | ||
33 | </div> | ||
34 | </div> | ||
35 | |||
36 | <div class="form-group"> | ||
37 | <label for="displayName" i18n>Channel display name</label> | ||
38 | |||
39 | <div class="input-group"> | ||
40 | <input | ||
41 | type="text" id="displayName" | ||
42 | formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" | ||
43 | > | ||
44 | </div> | ||
45 | |||
46 | <div *ngIf="formErrors.displayName" class="form-error"> | ||
47 | {{ formErrors.displayName }} | ||
48 | </div> | ||
49 | </div> | ||
50 | </form> | ||
diff --git a/client/src/app/+signup/+register/register-step-channel.component.ts b/client/src/app/+signup/+register/register-step-channel.component.ts new file mode 100644 index 000000000..9e13f75b3 --- /dev/null +++ b/client/src/app/+signup/+register/register-step-channel.component.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { FormReactive, VideoChannelValidatorsService } from '@app/shared' | ||
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
5 | import { FormGroup } from '@angular/forms' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-register-step-channel', | ||
9 | templateUrl: './register-step-channel.component.html', | ||
10 | styleUrls: [ './register.component.scss' ] | ||
11 | }) | ||
12 | export class RegisterStepChannelComponent extends FormReactive implements OnInit { | ||
13 | @Input() username: string | ||
14 | @Output() formBuilt = new EventEmitter<FormGroup>() | ||
15 | |||
16 | constructor ( | ||
17 | protected formValidatorService: FormValidatorService, | ||
18 | private authService: AuthService, | ||
19 | private videoChannelValidatorsService: VideoChannelValidatorsService | ||
20 | ) { | ||
21 | super() | ||
22 | } | ||
23 | |||
24 | get instanceHost () { | ||
25 | return window.location.host | ||
26 | } | ||
27 | |||
28 | isSameThanUsername () { | ||
29 | return this.username && this.username === this.form.value['name'] | ||
30 | } | ||
31 | |||
32 | ngOnInit () { | ||
33 | this.buildForm({ | ||
34 | name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME, | ||
35 | displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME | ||
36 | }) | ||
37 | |||
38 | setTimeout(() => this.formBuilt.emit(this.form)) | ||
39 | } | ||
40 | } | ||
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html new file mode 100644 index 000000000..cd0c78bfa --- /dev/null +++ b/client/src/app/+signup/+register/register-step-user.component.html | |||
@@ -0,0 +1,54 @@ | |||
1 | <form role="form" [formGroup]="form"> | ||
2 | |||
3 | <div class="form-group"> | ||
4 | <label for="username" i18n>Username</label> | ||
5 | |||
6 | <div class="input-group"> | ||
7 | <input | ||
8 | type="text" id="username" i18n-placeholder placeholder="Example: jane_doe" | ||
9 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" | ||
10 | > | ||
11 | <div class="input-group-append"> | ||
12 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
13 | </div> | ||
14 | </div> | ||
15 | |||
16 | <div *ngIf="formErrors.username" class="form-error"> | ||
17 | {{ formErrors.username }} | ||
18 | </div> | ||
19 | </div> | ||
20 | |||
21 | <div class="form-group"> | ||
22 | <label for="email" i18n>Email</label> | ||
23 | <input | ||
24 | type="text" id="email" i18n-placeholder placeholder="Email" | ||
25 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" | ||
26 | > | ||
27 | <div *ngIf="formErrors.email" class="form-error"> | ||
28 | {{ formErrors.email }} | ||
29 | </div> | ||
30 | </div> | ||
31 | |||
32 | <div class="form-group"> | ||
33 | <label for="password" i18n>Password</label> | ||
34 | <input | ||
35 | type="password" id="password" i18n-placeholder placeholder="Password" | ||
36 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
37 | > | ||
38 | <div *ngIf="formErrors.password" class="form-error"> | ||
39 | {{ formErrors.password }} | ||
40 | </div> | ||
41 | </div> | ||
42 | |||
43 | <div class="form-group form-group-terms"> | ||
44 | <my-peertube-checkbox | ||
45 | inputName="terms" formControlName="terms" | ||
46 | i18n-labelHtml | ||
47 | labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" | ||
48 | ></my-peertube-checkbox> | ||
49 | |||
50 | <div *ngIf="formErrors.terms" class="form-error"> | ||
51 | {{ formErrors.terms }} | ||
52 | </div> | ||
53 | </div> | ||
54 | </form> | ||
diff --git a/client/src/app/+signup/+register/register-step-user.component.ts b/client/src/app/+signup/+register/register-step-user.component.ts new file mode 100644 index 000000000..3825ae371 --- /dev/null +++ b/client/src/app/+signup/+register/register-step-user.component.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { FormReactive, UserValidatorsService } from '@app/shared' | ||
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
5 | import { FormGroup } from '@angular/forms' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-register-step-user', | ||
9 | templateUrl: './register-step-user.component.html', | ||
10 | styleUrls: [ './register.component.scss' ] | ||
11 | }) | ||
12 | export class RegisterStepUserComponent extends FormReactive implements OnInit { | ||
13 | @Output() formBuilt = new EventEmitter<FormGroup>() | ||
14 | |||
15 | constructor ( | ||
16 | protected formValidatorService: FormValidatorService, | ||
17 | private authService: AuthService, | ||
18 | private userValidatorsService: UserValidatorsService | ||
19 | ) { | ||
20 | super() | ||
21 | } | ||
22 | |||
23 | get instanceHost () { | ||
24 | return window.location.host | ||
25 | } | ||
26 | |||
27 | ngOnInit () { | ||
28 | this.buildForm({ | ||
29 | username: this.userValidatorsService.USER_USERNAME, | ||
30 | password: this.userValidatorsService.USER_PASSWORD, | ||
31 | email: this.userValidatorsService.USER_EMAIL, | ||
32 | terms: this.userValidatorsService.USER_TERMS | ||
33 | }) | ||
34 | |||
35 | setTimeout(() => this.formBuilt.emit(this.form)) | ||
36 | } | ||
37 | } | ||
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html new file mode 100644 index 000000000..24def68c1 --- /dev/null +++ b/client/src/app/+signup/+register/register.component.html | |||
@@ -0,0 +1,41 @@ | |||
1 | <div class="margin-content"> | ||
2 | |||
3 | <div i18n class="title-page title-page-single"> | ||
4 | Create an account | ||
5 | </div> | ||
6 | |||
7 | <my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success> | ||
8 | <div *ngIf="info" class="alert alert-info">{{ info }}</div> | ||
9 | |||
10 | <div class="wrapper" *ngIf="!signupDone"> | ||
11 | <div> | ||
12 | <my-custom-stepper linear *ngIf="!signupDone"> | ||
13 | <cdk-step [stepControl]="formStepUser" i18n-label label="User information"> | ||
14 | <my-register-step-user (formBuilt)="onUserFormBuilt($event)"></my-register-step-user> | ||
15 | |||
16 | <button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid">Next</button> | ||
17 | </cdk-step> | ||
18 | |||
19 | <cdk-step [stepControl]="formStepChannel" i18n-label label="Channel information"> | ||
20 | <my-register-step-channel (formBuilt)="onChannelFormBuilt($event)" [username]="getUsername()"></my-register-step-channel> | ||
21 | |||
22 | <button i18n cdkStepperNext (click)="signup()" | ||
23 | [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" | ||
24 | > | ||
25 | Create my account | ||
26 | </button> | ||
27 | </cdk-step> | ||
28 | |||
29 | <cdk-step i18n-label label="Done" editable="false"> | ||
30 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
31 | </cdk-step> | ||
32 | </my-custom-stepper> | ||
33 | </div> | ||
34 | |||
35 | <div> | ||
36 | <label i18n>Features found on this instance</label> | ||
37 | <my-instance-features-table></my-instance-features-table> | ||
38 | </div> | ||
39 | </div> | ||
40 | |||
41 | </div> | ||
diff --git a/client/src/app/signup/signup.component.scss b/client/src/app/+signup/+register/register.component.scss index 90e1e8e74..6f61b78f7 100644 --- a/client/src/app/signup/signup.component.scss +++ b/client/src/app/+signup/+register/register.component.scss | |||
@@ -1,16 +1,32 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .alert { | ||
5 | font-size: 15px; | ||
6 | text-align: center; | ||
7 | } | ||
8 | |||
9 | .wrapper { | ||
10 | display: flex; | ||
11 | justify-content: space-between; | ||
12 | flex-wrap: wrap; | ||
13 | |||
14 | & > div { | ||
15 | margin-bottom: 40px; | ||
16 | width: 450px; | ||
17 | |||
18 | @media screen and (max-width: 500px) { | ||
19 | width: auto; | ||
20 | } | ||
21 | } | ||
22 | } | ||
23 | |||
4 | my-instance-features-table { | 24 | my-instance-features-table { |
5 | display: block; | 25 | display: block; |
6 | 26 | ||
7 | margin-bottom: 40px; | 27 | margin-bottom: 40px; |
8 | } | 28 | } |
9 | 29 | ||
10 | form { | ||
11 | margin: 0 60px 40px 0; | ||
12 | } | ||
13 | |||
14 | .form-group-terms { | 30 | .form-group-terms { |
15 | margin: 30px 0; | 31 | margin: 30px 0; |
16 | } | 32 | } |
@@ -25,15 +41,18 @@ form { | |||
25 | 41 | ||
26 | input:not([type=submit]) { | 42 | input:not([type=submit]) { |
27 | @include peertube-input-text(400px); | 43 | @include peertube-input-text(400px); |
44 | |||
28 | display: block; | 45 | display: block; |
29 | 46 | ||
30 | &#username { | 47 | &#username, |
31 | width: auto; | 48 | &#name { |
49 | width: auto !important; | ||
32 | flex-grow: 1; | 50 | flex-grow: 1; |
33 | } | 51 | } |
34 | } | 52 | } |
35 | 53 | ||
36 | input[type=submit] { | 54 | input[type=submit], |
55 | button { | ||
37 | @include peertube-button; | 56 | @include peertube-button; |
38 | @include orange-button; | 57 | @include orange-button; |
39 | } | 58 | } |
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts new file mode 100644 index 000000000..cd6059728 --- /dev/null +++ b/client/src/app/+signup/+register/register.component.ts | |||
@@ -0,0 +1,89 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' | ||
3 | import { UserService, UserValidatorsService } from '@app/shared' | ||
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
5 | import { UserRegister } from '@shared/models/users/user-register.model' | ||
6 | import { FormGroup } from '@angular/forms' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-register', | ||
10 | templateUrl: './register.component.html', | ||
11 | styleUrls: [ './register.component.scss' ] | ||
12 | }) | ||
13 | export class RegisterComponent { | ||
14 | info: string = null | ||
15 | error: string = null | ||
16 | success: string = null | ||
17 | signupDone = false | ||
18 | |||
19 | formStepUser: FormGroup | ||
20 | formStepChannel: FormGroup | ||
21 | |||
22 | constructor ( | ||
23 | private authService: AuthService, | ||
24 | private userValidatorsService: UserValidatorsService, | ||
25 | private notifier: Notifier, | ||
26 | private userService: UserService, | ||
27 | private serverService: ServerService, | ||
28 | private redirectService: RedirectService, | ||
29 | private i18n: I18n | ||
30 | ) { | ||
31 | } | ||
32 | |||
33 | get requiresEmailVerification () { | ||
34 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
35 | } | ||
36 | |||
37 | hasSameChannelAndAccountNames () { | ||
38 | return this.getUsername() === this.getChannelName() | ||
39 | } | ||
40 | |||
41 | getUsername () { | ||
42 | if (!this.formStepUser) return undefined | ||
43 | |||
44 | return this.formStepUser.value['username'] | ||
45 | } | ||
46 | |||
47 | getChannelName () { | ||
48 | if (!this.formStepChannel) return undefined | ||
49 | |||
50 | return this.formStepChannel.value['name'] | ||
51 | } | ||
52 | |||
53 | onUserFormBuilt (form: FormGroup) { | ||
54 | this.formStepUser = form | ||
55 | } | ||
56 | |||
57 | onChannelFormBuilt (form: FormGroup) { | ||
58 | this.formStepChannel = form | ||
59 | } | ||
60 | |||
61 | signup () { | ||
62 | this.error = null | ||
63 | |||
64 | const body: UserRegister = Object.assign(this.formStepUser.value, { channel: this.formStepChannel.value }) | ||
65 | |||
66 | this.userService.signup(body).subscribe( | ||
67 | () => { | ||
68 | this.signupDone = true | ||
69 | |||
70 | if (this.requiresEmailVerification) { | ||
71 | this.info = this.i18n('Now please check your emails to verify your account and complete signup.') | ||
72 | return | ||
73 | } | ||
74 | |||
75 | // Auto login | ||
76 | this.authService.login(body.username, body.password) | ||
77 | .subscribe( | ||
78 | () => { | ||
79 | this.success = this.i18n('You are now logged in as {{username}}!', { username: body.username }) | ||
80 | }, | ||
81 | |||
82 | err => this.error = err.message | ||
83 | ) | ||
84 | }, | ||
85 | |||
86 | err => this.error = err.message | ||
87 | ) | ||
88 | } | ||
89 | } | ||
diff --git a/client/src/app/+signup/+register/register.module.ts b/client/src/app/+signup/+register/register.module.ts new file mode 100644 index 000000000..46336cbd0 --- /dev/null +++ b/client/src/app/+signup/+register/register.module.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RegisterRoutingModule } from './register-routing.module' | ||
3 | import { RegisterComponent } from './register.component' | ||
4 | import { SharedModule } from '@app/shared' | ||
5 | import { CdkStepperModule } from '@angular/cdk/stepper' | ||
6 | import { RegisterStepChannelComponent } from './register-step-channel.component' | ||
7 | import { RegisterStepUserComponent } from './register-step-user.component' | ||
8 | import { CustomStepperComponent } from './custom-stepper.component' | ||
9 | import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' | ||
10 | |||
11 | @NgModule({ | ||
12 | imports: [ | ||
13 | RegisterRoutingModule, | ||
14 | SharedModule, | ||
15 | CdkStepperModule, | ||
16 | SignupSharedModule | ||
17 | ], | ||
18 | |||
19 | declarations: [ | ||
20 | RegisterComponent, | ||
21 | CustomStepperComponent, | ||
22 | RegisterStepChannelComponent, | ||
23 | RegisterStepUserComponent | ||
24 | ], | ||
25 | |||
26 | exports: [ | ||
27 | RegisterComponent | ||
28 | ], | ||
29 | |||
30 | providers: [ | ||
31 | ] | ||
32 | }) | ||
33 | export class RegisterModule { } | ||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html index 2e4180632..2e4180632 100644 --- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html | |||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss index efec6b706..efec6b706 100644 --- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss | |||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts index cfd471fa4..cfd471fa4 100644 --- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts | |||
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html index a83d4a3c2..728709ca6 100644 --- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html | |||
@@ -3,9 +3,9 @@ | |||
3 | Verify account email confirmation | 3 | Verify account email confirmation |
4 | </div> | 4 | </div> |
5 | 5 | ||
6 | <div i18n *ngIf="success; else verificationError"> | 6 | <my-signup-success i18n *ngIf="success; else verificationError" message="Your email has been verified and you may now login."> |
7 | Your email has been verified and you may now login. Redirecting... | 7 | </my-signup-success> |
8 | </div> | 8 | |
9 | <ng-template #verificationError> | 9 | <ng-template #verificationError> |
10 | <div> | 10 | <div> |
11 | <span i18n>An error occurred. </span> | 11 | <span i18n>An error occurred. </span> |
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts index f9ecf664b..3fb2d1cd8 100644 --- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts | |||
@@ -40,9 +40,6 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
40 | .subscribe( | 40 | .subscribe( |
41 | () => { | 41 | () => { |
42 | this.success = true | 42 | this.success = true |
43 | setTimeout(() => { | ||
44 | this.router.navigate([ '/login' ]) | ||
45 | }, 2000) | ||
46 | }, | 43 | }, |
47 | 44 | ||
48 | err => { | 45 | err => { |
diff --git a/client/src/app/+verify-account/verify-account-routing.module.ts b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts index a038f0336..16d5fe0d0 100644 --- a/client/src/app/+verify-account/verify-account-routing.module.ts +++ b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts | |||
@@ -1,12 +1,8 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | |||
4 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
5 | 4 | import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component' | |
6 | import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component' | 5 | import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' |
7 | import { | ||
8 | VerifyAccountAskSendEmailComponent | ||
9 | } from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component' | ||
10 | 6 | ||
11 | const verifyAccountRoutes: Routes = [ | 7 | const verifyAccountRoutes: Routes = [ |
12 | { | 8 | { |
diff --git a/client/src/app/+signup/+verify-account/verify-account.module.ts b/client/src/app/+signup/+verify-account/verify-account.module.ts new file mode 100644 index 000000000..9fe14e81e --- /dev/null +++ b/client/src/app/+signup/+verify-account/verify-account.module.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { VerifyAccountRoutingModule } from './verify-account-routing.module' | ||
3 | import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component' | ||
4 | import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' | ||
5 | import { SharedModule } from '@app/shared' | ||
6 | import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' | ||
7 | |||
8 | @NgModule({ | ||
9 | imports: [ | ||
10 | VerifyAccountRoutingModule, | ||
11 | SharedModule, | ||
12 | SignupSharedModule | ||
13 | ], | ||
14 | |||
15 | declarations: [ | ||
16 | VerifyAccountEmailComponent, | ||
17 | VerifyAccountAskSendEmailComponent | ||
18 | ], | ||
19 | |||
20 | exports: [], | ||
21 | |||
22 | providers: [] | ||
23 | }) | ||
24 | export class VerifyAccountModule { | ||
25 | } | ||
diff --git a/client/src/app/+signup/shared/signup-shared.module.ts b/client/src/app/+signup/shared/signup-shared.module.ts new file mode 100644 index 000000000..cd21fdef3 --- /dev/null +++ b/client/src/app/+signup/shared/signup-shared.module.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SignupSuccessComponent } from '../shared/signup-success.component' | ||
3 | import { SharedModule } from '@app/shared' | ||
4 | |||
5 | @NgModule({ | ||
6 | imports: [ | ||
7 | SharedModule | ||
8 | ], | ||
9 | |||
10 | declarations: [ | ||
11 | SignupSuccessComponent | ||
12 | ], | ||
13 | |||
14 | exports: [ | ||
15 | SignupSuccessComponent | ||
16 | ], | ||
17 | |||
18 | providers: [ | ||
19 | ] | ||
20 | }) | ||
21 | export class SignupSharedModule { } | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.html b/client/src/app/+signup/shared/signup-success.component.html new file mode 100644 index 000000000..e35f858c6 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success.component.html | |||
@@ -0,0 +1,16 @@ | |||
1 | <!-- Thanks: Amit Singh Sansoya from https://codepen.io/amit3200/pen/zWMJOO --> | ||
2 | |||
3 | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2"> | ||
4 | <circle class="path circle" fill="none" stroke="#73AF55" stroke-width="6" stroke-miterlimit="10" cx="65.1" cy="65.1" r="62.1"/> | ||
5 | <polyline class="path check" fill="none" stroke="#73AF55" stroke-width="6" stroke-linecap="round" stroke-miterlimit="10" points="100.2,40.2 51.5,88.8 29.8,67.5 "/> | ||
6 | </svg> | ||
7 | |||
8 | <p class="bottom-message">Welcome on PeerTube!</p> | ||
9 | |||
10 | <div *ngIf="message" class="alert alert-success"> | ||
11 | <p>{{ message }}</p> | ||
12 | |||
13 | <p i18n> | ||
14 | If you need help to use PeerTube, you can take a look to the <a href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
15 | </p> | ||
16 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.scss b/client/src/app/+signup/shared/signup-success.component.scss new file mode 100644 index 000000000..fbc27c8bc --- /dev/null +++ b/client/src/app/+signup/shared/signup-success.component.scss | |||
@@ -0,0 +1,76 @@ | |||
1 | svg { | ||
2 | width: 100px; | ||
3 | display: block; | ||
4 | margin: 40px auto 0; | ||
5 | } | ||
6 | |||
7 | .path { | ||
8 | stroke-dasharray: 1000; | ||
9 | stroke-dashoffset: 0; | ||
10 | |||
11 | &.circle { | ||
12 | -webkit-animation: dash .9s ease-in-out; | ||
13 | animation: dash .9s ease-in-out; | ||
14 | } | ||
15 | |||
16 | &.line { | ||
17 | stroke-dashoffset: 1000; | ||
18 | -webkit-animation: dash .9s .35s ease-in-out forwards; | ||
19 | animation: dash .9s .35s ease-in-out forwards; | ||
20 | } | ||
21 | |||
22 | &.check { | ||
23 | stroke-dashoffset: -100; | ||
24 | -webkit-animation: dash-check .9s .35s ease-in-out forwards; | ||
25 | animation: dash-check .9s .35s ease-in-out forwards; | ||
26 | } | ||
27 | } | ||
28 | |||
29 | .bottom-message { | ||
30 | text-align: center; | ||
31 | margin: 20px 0 60px; | ||
32 | font-size: 1.25em; | ||
33 | color: #73AF55; | ||
34 | } | ||
35 | |||
36 | .alert { | ||
37 | font-size: 15px; | ||
38 | text-align: center; | ||
39 | } | ||
40 | |||
41 | |||
42 | @-webkit-keyframes dash { | ||
43 | 0% { | ||
44 | stroke-dashoffset: 1000; | ||
45 | } | ||
46 | 100% { | ||
47 | stroke-dashoffset: 0; | ||
48 | } | ||
49 | } | ||
50 | |||
51 | @keyframes dash { | ||
52 | 0% { | ||
53 | stroke-dashoffset: 1000; | ||
54 | } | ||
55 | 100% { | ||
56 | stroke-dashoffset: 0; | ||
57 | } | ||
58 | } | ||
59 | |||
60 | @-webkit-keyframes dash-check { | ||
61 | 0% { | ||
62 | stroke-dashoffset: -100; | ||
63 | } | ||
64 | 100% { | ||
65 | stroke-dashoffset: 900; | ||
66 | } | ||
67 | } | ||
68 | |||
69 | @keyframes dash-check { | ||
70 | 0% { | ||
71 | stroke-dashoffset: -100; | ||
72 | } | ||
73 | 100% { | ||
74 | stroke-dashoffset: 900; | ||
75 | } | ||
76 | } | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.ts b/client/src/app/+signup/shared/signup-success.component.ts new file mode 100644 index 000000000..19fb5922a --- /dev/null +++ b/client/src/app/+signup/shared/signup-success.component.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-success', | ||
5 | templateUrl: './signup-success.component.html', | ||
6 | styleUrls: [ './signup-success.component.scss' ] | ||
7 | }) | ||
8 | export class SignupSuccessComponent { | ||
9 | @Input() message: string | ||
10 | } | ||
diff --git a/client/src/app/+verify-account/index.ts b/client/src/app/+verify-account/index.ts deleted file mode 100644 index 733f5ba77..000000000 --- a/client/src/app/+verify-account/index.ts +++ /dev/null | |||
@@ -1,2 +0,0 @@ | |||
1 | export * from '@app/+verify-account/verify-account-routing.module' | ||
2 | export * from '@app/+verify-account/verify-account.module' | ||
diff --git a/client/src/app/+verify-account/verify-account.module.ts b/client/src/app/+verify-account/verify-account.module.ts deleted file mode 100644 index 9092c6b4f..000000000 --- a/client/src/app/+verify-account/verify-account.module.ts +++ /dev/null | |||
@@ -1,27 +0,0 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | |||
3 | import { VerifyAccountRoutingModule } from '@app/+verify-account/verify-account-routing.module' | ||
4 | import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component' | ||
5 | import { | ||
6 | VerifyAccountAskSendEmailComponent | ||
7 | } from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component' | ||
8 | import { SharedModule } from '@app/shared' | ||
9 | |||
10 | @NgModule({ | ||
11 | imports: [ | ||
12 | VerifyAccountRoutingModule, | ||
13 | SharedModule | ||
14 | ], | ||
15 | |||
16 | declarations: [ | ||
17 | VerifyAccountEmailComponent, | ||
18 | VerifyAccountAskSendEmailComponent | ||
19 | ], | ||
20 | |||
21 | exports: [ | ||
22 | ], | ||
23 | |||
24 | providers: [ | ||
25 | ] | ||
26 | }) | ||
27 | export class VerifyAccountModule { } | ||
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts index 907aefae1..7990044a2 100644 --- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts +++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts | |||
@@ -5,7 +5,7 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | |||
5 | import { Subscription } from 'rxjs' | 5 | import { Subscription } from 'rxjs' |
6 | import { Notifier } from '@app/core' | 6 | import { Notifier } from '@app/core' |
7 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 7 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
8 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 8 | import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' |
9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
@@ -46,8 +46,7 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy { | |||
46 | } | 46 | } |
47 | 47 | ||
48 | onNearOfBottom () { | 48 | onNearOfBottom () { |
49 | // Last page | 49 | if (!hasMoreItems(this.pagination)) return |
50 | if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return | ||
51 | 50 | ||
52 | this.pagination.currentPage += 1 | 51 | this.pagination.currentPage += 1 |
53 | this.loadVideoPlaylists() | 52 | this.loadVideoPlaylists() |
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index 5e60b34b4..629fd4450 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts | |||
@@ -29,6 +29,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
29 | private videoChannelSub: Subscription | 29 | private videoChannelSub: Subscription |
30 | 30 | ||
31 | constructor ( | 31 | constructor ( |
32 | protected i18n: I18n, | ||
32 | protected router: Router, | 33 | protected router: Router, |
33 | protected serverService: ServerService, | 34 | protected serverService: ServerService, |
34 | protected route: ActivatedRoute, | 35 | protected route: ActivatedRoute, |
@@ -36,7 +37,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
36 | protected notifier: Notifier, | 37 | protected notifier: Notifier, |
37 | protected confirmService: ConfirmService, | 38 | protected confirmService: ConfirmService, |
38 | protected screenService: ScreenService, | 39 | protected screenService: ScreenService, |
39 | private i18n: I18n, | ||
40 | private videoChannelService: VideoChannelService, | 40 | private videoChannelService: VideoChannelService, |
41 | private videoService: VideoService | 41 | private videoService: VideoService |
42 | ) { | 42 | ) { |
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index db8888dba..7ca51f226 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts | |||
@@ -16,7 +16,7 @@ const routes: Routes = [ | |||
16 | }, | 16 | }, |
17 | { | 17 | { |
18 | path: 'verify-account', | 18 | path: 'verify-account', |
19 | loadChildren: './+verify-account/verify-account.module#VerifyAccountModule' | 19 | loadChildren: './+signup/+verify-account/verify-account.module#VerifyAccountModule' |
20 | }, | 20 | }, |
21 | { | 21 | { |
22 | path: 'accounts', | 22 | path: 'accounts', |
@@ -31,6 +31,10 @@ const routes: Routes = [ | |||
31 | loadChildren: './+about/about.module#AboutModule' | 31 | loadChildren: './+about/about.module#AboutModule' |
32 | }, | 32 | }, |
33 | { | 33 | { |
34 | path: 'signup', | ||
35 | loadChildren: './+signup/+register/register.module#RegisterModule' | ||
36 | }, | ||
37 | { | ||
34 | path: '', | 38 | path: '', |
35 | component: AppComponent // Avoid 404, app component will redirect dynamically | 39 | component: AppComponent // Avoid 404, app component will redirect dynamically |
36 | }, | 40 | }, |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 0bbc2e08b..1e2936a37 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -14,7 +14,6 @@ import { HeaderComponent } from './header' | |||
14 | import { LoginModule } from './login' | 14 | import { LoginModule } from './login' |
15 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' | 15 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' |
16 | import { SharedModule } from './shared' | 16 | import { SharedModule } from './shared' |
17 | import { SignupModule } from './signup' | ||
18 | import { VideosModule } from './videos' | 17 | import { VideosModule } from './videos' |
19 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' | 18 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' |
20 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | 19 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' |
@@ -53,7 +52,6 @@ export function metaFactory (serverService: ServerService): MetaLoader { | |||
53 | CoreModule, | 52 | CoreModule, |
54 | LoginModule, | 53 | LoginModule, |
55 | ResetPasswordModule, | 54 | ResetPasswordModule, |
56 | SignupModule, | ||
57 | SearchModule, | 55 | SearchModule, |
58 | SharedModule, | 56 | SharedModule, |
59 | VideosModule, | 57 | VideosModule, |
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index d3e72afb4..06fa8fcf1 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -20,6 +20,7 @@ import { Notifier } from './notification' | |||
20 | import { MessageService } from 'primeng/api' | 20 | import { MessageService } from 'primeng/api' |
21 | import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' | 21 | import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' |
22 | import { ServerConfigResolver } from './routing/server-config-resolver.service' | 22 | import { ServerConfigResolver } from './routing/server-config-resolver.service' |
23 | import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' | ||
23 | 24 | ||
24 | @NgModule({ | 25 | @NgModule({ |
25 | imports: [ | 26 | imports: [ |
@@ -58,6 +59,8 @@ import { ServerConfigResolver } from './routing/server-config-resolver.service' | |||
58 | ThemeService, | 59 | ThemeService, |
59 | LoginGuard, | 60 | LoginGuard, |
60 | UserRightGuard, | 61 | UserRightGuard, |
62 | UnloggedGuard, | ||
63 | |||
61 | RedirectService, | 64 | RedirectService, |
62 | Notifier, | 65 | Notifier, |
63 | MessageService, | 66 | MessageService, |
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts index e1db4097b..571822b76 100644 --- a/client/src/app/core/routing/redirect.service.ts +++ b/client/src/app/core/routing/redirect.service.ts | |||
@@ -42,7 +42,14 @@ export class RedirectService { | |||
42 | } | 42 | } |
43 | 43 | ||
44 | redirectToPreviousRoute () { | 44 | redirectToPreviousRoute () { |
45 | if (this.previousUrl) return this.router.navigateByUrl(this.previousUrl) | 45 | const exceptions = [ |
46 | '/verify-account' | ||
47 | ] | ||
48 | |||
49 | if (this.previousUrl) { | ||
50 | const isException = exceptions.find(e => this.previousUrl.startsWith(e)) | ||
51 | if (!isException) return this.router.navigateByUrl(this.previousUrl) | ||
52 | } | ||
46 | 53 | ||
47 | return this.redirectToHomepage() | 54 | return this.redirectToHomepage() |
48 | } | 55 | } |
diff --git a/client/src/app/core/routing/unlogged-guard.service.ts b/client/src/app/core/routing/unlogged-guard.service.ts new file mode 100644 index 000000000..3132a1a77 --- /dev/null +++ b/client/src/app/core/routing/unlogged-guard.service.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router' | ||
3 | import { AuthService } from '../auth/auth.service' | ||
4 | import { RedirectService } from './redirect.service' | ||
5 | |||
6 | @Injectable() | ||
7 | export class UnloggedGuard implements CanActivate, CanActivateChild { | ||
8 | |||
9 | constructor ( | ||
10 | private router: Router, | ||
11 | private auth: AuthService, | ||
12 | private redirectService: RedirectService | ||
13 | ) {} | ||
14 | |||
15 | canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | ||
16 | if (this.auth.isLoggedIn() === false) return true | ||
17 | |||
18 | this.redirectService.redirectToHomepage() | ||
19 | return false | ||
20 | } | ||
21 | |||
22 | canActivateChild (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | ||
23 | return this.canActivate(route, state) | ||
24 | } | ||
25 | } | ||
diff --git a/client/src/app/shared/actor/actor.model.ts b/client/src/app/shared/actor/actor.model.ts index adecec1fc..5a517c975 100644 --- a/client/src/app/shared/actor/actor.model.ts +++ b/client/src/app/shared/actor/actor.model.ts | |||
@@ -4,7 +4,6 @@ import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | |||
4 | 4 | ||
5 | export abstract class Actor implements ActorServer { | 5 | export abstract class Actor implements ActorServer { |
6 | id: number | 6 | id: number |
7 | uuid: string | ||
8 | url: string | 7 | url: string |
9 | name: string | 8 | name: string |
10 | host: string | 9 | host: string |
@@ -35,7 +34,6 @@ export abstract class Actor implements ActorServer { | |||
35 | 34 | ||
36 | protected constructor (hash: ActorServer) { | 35 | protected constructor (hash: ActorServer) { |
37 | this.id = hash.id | 36 | this.id = hash.id |
38 | this.uuid = hash.uuid | ||
39 | this.url = hash.url | 37 | this.url = hash.url |
40 | this.name = hash.name | 38 | this.name = hash.name |
41 | this.host = hash.host | 39 | this.host = hash.host |
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss index 04199a2a9..99d7f51c1 100644 --- a/client/src/app/shared/buttons/button.component.scss +++ b/client/src/app/shared/buttons/button.component.scss | |||
@@ -5,16 +5,9 @@ | |||
5 | @include peertube-button-link; | 5 | @include peertube-button-link; |
6 | @include button-with-icon(21px, 0, -2px); | 6 | @include button-with-icon(21px, 0, -2px); |
7 | 7 | ||
8 | font-weight: $font-semibold; | 8 | // FIXME: Firefox does not apply global .orange-button icon color |
9 | color: $grey-foreground-color; | 9 | &.orange-button { |
10 | background-color: $grey-background-color; | 10 | @include apply-svg-color(#fff) |
11 | |||
12 | &:hover { | ||
13 | background-color: $grey-background-hover-color; | ||
14 | } | ||
15 | |||
16 | my-global-icon { | ||
17 | @include apply-svg-color($grey-foreground-color); | ||
18 | } | 11 | } |
19 | } | 12 | } |
20 | 13 | ||
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts index c2b69d31a..cf334e8d5 100644 --- a/client/src/app/shared/buttons/button.component.ts +++ b/client/src/app/shared/buttons/button.component.ts | |||
@@ -9,7 +9,7 @@ import { GlobalIconName } from '@app/shared/images/global-icon.component' | |||
9 | 9 | ||
10 | export class ButtonComponent { | 10 | export class ButtonComponent { |
11 | @Input() label = '' | 11 | @Input() label = '' |
12 | @Input() className: string = undefined | 12 | @Input() className = 'grey-button' |
13 | @Input() icon: GlobalIconName = undefined | 13 | @Input() icon: GlobalIconName = undefined |
14 | @Input() title: string = undefined | 14 | @Input() title: string = undefined |
15 | 15 | ||
diff --git a/client/src/app/shared/buttons/delete-button.component.html b/client/src/app/shared/buttons/delete-button.component.html index b4acb9d32..25196fbd5 100644 --- a/client/src/app/shared/buttons/delete-button.component.html +++ b/client/src/app/shared/buttons/delete-button.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <span class="action-button action-button-delete" [title]="title" role="button"> | 1 | <span class="action-button action-button-delete grey-button" [title]="title" role="button"> |
2 | <my-global-icon iconName="delete"></my-global-icon> | 2 | <my-global-icon iconName="delete"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
diff --git a/client/src/app/shared/buttons/edit-button.component.html b/client/src/app/shared/buttons/edit-button.component.html index da3addbae..3d7cd4780 100644 --- a/client/src/app/shared/buttons/edit-button.component.html +++ b/client/src/app/shared/buttons/edit-button.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> | 1 | <a class="action-button action-button-edit grey-button" [routerLink]="routerLink" i18n-title title="Edit"> |
2 | <my-global-icon iconName="edit"></my-global-icon> | 2 | <my-global-icon iconName="edit"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.scss b/client/src/app/shared/forms/peertube-checkbox.component.scss index ea321ee65..84ea788af 100644 --- a/client/src/app/shared/forms/peertube-checkbox.component.scss +++ b/client/src/app/shared/forms/peertube-checkbox.component.scss | |||
@@ -14,9 +14,6 @@ | |||
14 | 14 | ||
15 | input { | 15 | input { |
16 | @include peertube-checkbox(1px); | 16 | @include peertube-checkbox(1px); |
17 | |||
18 | width: 10px; | ||
19 | margin-right: 10px; | ||
20 | } | 17 | } |
21 | } | 18 | } |
22 | 19 | ||
diff --git a/client/src/app/shared/forms/reactive-file.component.html b/client/src/app/shared/forms/reactive-file.component.html index 7d691059d..f6bf5f9ae 100644 --- a/client/src/app/shared/forms/reactive-file.component.html +++ b/client/src/app/shared/forms/reactive-file.component.html | |||
@@ -1,6 +1,9 @@ | |||
1 | <div class="root"> | 1 | <div class="root"> |
2 | <div class="button-file"> | 2 | <div class="button-file" [ngClass]="{ 'with-icon': !!icon }"> |
3 | <my-global-icon *ngIf="icon" [iconName]="icon"></my-global-icon> | ||
4 | |||
3 | <span>{{ inputLabel }}</span> | 5 | <span>{{ inputLabel }}</span> |
6 | |||
4 | <input | 7 | <input |
5 | type="file" | 8 | type="file" |
6 | [name]="inputName" [id]="inputName" [accept]="extensions" | 9 | [name]="inputName" [id]="inputName" [accept]="extensions" |
@@ -8,7 +11,5 @@ | |||
8 | /> | 11 | /> |
9 | </div> | 12 | </div> |
10 | 13 | ||
11 | <div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxFileSize | bytes }})</div> | ||
12 | |||
13 | <div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div> | 14 | <div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div> |
14 | </div> | 15 | </div> |
diff --git a/client/src/app/shared/forms/reactive-file.component.scss b/client/src/app/shared/forms/reactive-file.component.scss index d89844264..84c23c1d6 100644 --- a/client/src/app/shared/forms/reactive-file.component.scss +++ b/client/src/app/shared/forms/reactive-file.component.scss | |||
@@ -8,13 +8,11 @@ | |||
8 | 8 | ||
9 | .button-file { | 9 | .button-file { |
10 | @include peertube-button-file(auto); | 10 | @include peertube-button-file(auto); |
11 | @include grey-button; | ||
11 | 12 | ||
12 | min-width: 190px; | 13 | &.with-icon { |
13 | } | 14 | @include button-with-icon; |
14 | 15 | } | |
15 | .file-constraints { | ||
16 | margin-left: 5px; | ||
17 | font-size: 13px; | ||
18 | } | 16 | } |
19 | 17 | ||
20 | .filename { | 18 | .filename { |
diff --git a/client/src/app/shared/forms/reactive-file.component.ts b/client/src/app/shared/forms/reactive-file.component.ts index f60c38e8d..b7a821d4f 100644 --- a/client/src/app/shared/forms/reactive-file.component.ts +++ b/client/src/app/shared/forms/reactive-file.component.ts | |||
@@ -2,6 +2,7 @@ import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@ang | |||
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { GlobalIconName } from '@app/shared/images/global-icon.component' | ||
5 | 6 | ||
6 | @Component({ | 7 | @Component({ |
7 | selector: 'my-reactive-file', | 8 | selector: 'my-reactive-file', |
@@ -21,6 +22,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor { | |||
21 | @Input() extensions: string[] = [] | 22 | @Input() extensions: string[] = [] |
22 | @Input() maxFileSize: number | 23 | @Input() maxFileSize: number |
23 | @Input() displayFilename = false | 24 | @Input() displayFilename = false |
25 | @Input() icon: GlobalIconName | ||
24 | 26 | ||
25 | @Output() fileChanged = new EventEmitter<Blob>() | 27 | @Output() fileChanged = new EventEmitter<Blob>() |
26 | 28 | ||
diff --git a/client/src/app/shared/images/image-upload.component.html b/client/src/app/shared/images/image-upload.component.html deleted file mode 100644 index c09c862c4..000000000 --- a/client/src/app/shared/images/image-upload.component.html +++ /dev/null | |||
@@ -1,9 +0,0 @@ | |||
1 | <div class="root"> | ||
2 | <my-reactive-file | ||
3 | [inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize" | ||
4 | (fileChanged)="onFileChanged($event)" | ||
5 | ></my-reactive-file> | ||
6 | |||
7 | <img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" /> | ||
8 | <div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div> | ||
9 | </div> | ||
diff --git a/client/src/app/shared/images/image-upload.component.scss b/client/src/app/shared/images/image-upload.component.scss deleted file mode 100644 index b63963bca..000000000 --- a/client/src/app/shared/images/image-upload.component.scss +++ /dev/null | |||
@@ -1,18 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .root { | ||
5 | height: auto; | ||
6 | display: flex; | ||
7 | align-items: center; | ||
8 | |||
9 | .preview { | ||
10 | border: 2px solid grey; | ||
11 | border-radius: 4px; | ||
12 | margin-left: 50px; | ||
13 | |||
14 | &.no-image { | ||
15 | background-color: #ececec; | ||
16 | } | ||
17 | } | ||
18 | } | ||
diff --git a/client/src/app/shared/images/preview-upload.component.html b/client/src/app/shared/images/preview-upload.component.html new file mode 100644 index 000000000..5e1d5211b --- /dev/null +++ b/client/src/app/shared/images/preview-upload.component.html | |||
@@ -0,0 +1,13 @@ | |||
1 | <div class="root"> | ||
2 | <div class="preview-container"> | ||
3 | <my-reactive-file | ||
4 | [inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize" | ||
5 | icon="edit" (fileChanged)="onFileChanged($event)" | ||
6 | ></my-reactive-file> | ||
7 | |||
8 | <img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" /> | ||
9 | <div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div> | ||
10 | </div> | ||
11 | |||
12 | <div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxVideoImageSize | bytes }})</div> | ||
13 | </div> | ||
diff --git a/client/src/app/shared/images/preview-upload.component.scss b/client/src/app/shared/images/preview-upload.component.scss new file mode 100644 index 000000000..257060239 --- /dev/null +++ b/client/src/app/shared/images/preview-upload.component.scss | |||
@@ -0,0 +1,27 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .root { | ||
5 | height: auto; | ||
6 | display: flex; | ||
7 | flex-direction: column; | ||
8 | |||
9 | .preview-container { | ||
10 | position: relative; | ||
11 | |||
12 | my-reactive-file { | ||
13 | position: absolute; | ||
14 | bottom: 10px; | ||
15 | left: 10px; | ||
16 | } | ||
17 | |||
18 | .preview { | ||
19 | border: 2px solid grey; | ||
20 | border-radius: 4px; | ||
21 | |||
22 | &.no-image { | ||
23 | background-color: #ececec; | ||
24 | } | ||
25 | } | ||
26 | } | ||
27 | } | ||
diff --git a/client/src/app/shared/images/image-upload.component.ts b/client/src/app/shared/images/preview-upload.component.ts index 2da1592ff..44b78866e 100644 --- a/client/src/app/shared/images/image-upload.component.ts +++ b/client/src/app/shared/images/preview-upload.component.ts | |||
@@ -1,27 +1,28 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, forwardRef, Input, OnInit } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' | 3 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' |
4 | import { ServerService } from '@app/core' | 4 | import { ServerService } from '@app/core' |
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'my-image-upload', | 7 | selector: 'my-preview-upload', |
8 | styleUrls: [ './image-upload.component.scss' ], | 8 | styleUrls: [ './preview-upload.component.scss' ], |
9 | templateUrl: './image-upload.component.html', | 9 | templateUrl: './preview-upload.component.html', |
10 | providers: [ | 10 | providers: [ |
11 | { | 11 | { |
12 | provide: NG_VALUE_ACCESSOR, | 12 | provide: NG_VALUE_ACCESSOR, |
13 | useExisting: forwardRef(() => ImageUploadComponent), | 13 | useExisting: forwardRef(() => PreviewUploadComponent), |
14 | multi: true | 14 | multi: true |
15 | } | 15 | } |
16 | ] | 16 | ] |
17 | }) | 17 | }) |
18 | export class ImageUploadComponent implements ControlValueAccessor { | 18 | export class PreviewUploadComponent implements OnInit, ControlValueAccessor { |
19 | @Input() inputLabel: string | 19 | @Input() inputLabel: string |
20 | @Input() inputName: string | 20 | @Input() inputName: string |
21 | @Input() previewWidth: string | 21 | @Input() previewWidth: string |
22 | @Input() previewHeight: string | 22 | @Input() previewHeight: string |
23 | 23 | ||
24 | imageSrc: SafeResourceUrl | 24 | imageSrc: SafeResourceUrl |
25 | allowedExtensionsMessage = '' | ||
25 | 26 | ||
26 | private file: File | 27 | private file: File |
27 | 28 | ||
@@ -38,6 +39,10 @@ export class ImageUploadComponent implements ControlValueAccessor { | |||
38 | return this.serverService.getConfig().video.image.size.max | 39 | return this.serverService.getConfig().video.image.size.max |
39 | } | 40 | } |
40 | 41 | ||
42 | ngOnInit () { | ||
43 | this.allowedExtensionsMessage = this.videoImageExtensions.join(', ') | ||
44 | } | ||
45 | |||
41 | onFileChanged (file: File) { | 46 | onFileChanged (file: File) { |
42 | this.file = file | 47 | this.file = file |
43 | 48 | ||
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index ded65653f..39f1a69e2 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -69,7 +69,7 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha | |||
69 | import { ConfirmComponent } from '@app/shared/confirm/confirm.component' | 69 | import { ConfirmComponent } from '@app/shared/confirm/confirm.component' |
70 | import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' | 70 | import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' |
71 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 71 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
72 | import { ImageUploadComponent } from '@app/shared/images/image-upload.component' | 72 | import { PreviewUploadComponent } from '@app/shared/images/preview-upload.component' |
73 | import { GlobalIconComponent } from '@app/shared/images/global-icon.component' | 73 | import { GlobalIconComponent } from '@app/shared/images/global-icon.component' |
74 | import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component' | 74 | import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component' |
75 | import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' | 75 | import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' |
@@ -154,7 +154,7 @@ import { ClipboardModule } from 'ngx-clipboard' | |||
154 | ConfirmComponent, | 154 | ConfirmComponent, |
155 | 155 | ||
156 | GlobalIconComponent, | 156 | GlobalIconComponent, |
157 | ImageUploadComponent | 157 | PreviewUploadComponent |
158 | ], | 158 | ], |
159 | 159 | ||
160 | exports: [ | 160 | exports: [ |
@@ -218,7 +218,7 @@ import { ClipboardModule } from 'ngx-clipboard' | |||
218 | ConfirmComponent, | 218 | ConfirmComponent, |
219 | 219 | ||
220 | GlobalIconComponent, | 220 | GlobalIconComponent, |
221 | ImageUploadComponent, | 221 | PreviewUploadComponent, |
222 | 222 | ||
223 | NumberFormatterPipe, | 223 | NumberFormatterPipe, |
224 | ObjectLengthPipe, | 224 | ObjectLengthPipe, |
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index cc5c051f1..20883456f 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -9,6 +9,7 @@ import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | |||
9 | import { SortMeta } from 'primeng/api' | 9 | import { SortMeta } from 'primeng/api' |
10 | import { BytesPipe } from 'ngx-pipes' | 10 | import { BytesPipe } from 'ngx-pipes' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
12 | import { UserRegister } from '@shared/models/users/user-register.model' | ||
12 | 13 | ||
13 | @Injectable() | 14 | @Injectable() |
14 | export class UserService { | 15 | export class UserService { |
@@ -64,7 +65,7 @@ export class UserService { | |||
64 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 65 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
65 | } | 66 | } |
66 | 67 | ||
67 | signup (userCreate: UserCreate) { | 68 | signup (userCreate: UserRegister) { |
68 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) | 69 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) |
69 | .pipe( | 70 | .pipe( |
70 | map(this.restExtractor.extractDataBool), | 71 | map(this.restExtractor.extractDataBool), |
diff --git a/client/src/app/shared/video-channel/video-channel.service.ts b/client/src/app/shared/video-channel/video-channel.service.ts index d0bec649a..0168d37d9 100644 --- a/client/src/app/shared/video-channel/video-channel.service.ts +++ b/client/src/app/shared/video-channel/video-channel.service.ts | |||
@@ -2,7 +2,7 @@ import { catchError, map, tap } from 'rxjs/operators' | |||
2 | import { Injectable } from '@angular/core' | 2 | import { Injectable } from '@angular/core' |
3 | import { Observable, ReplaySubject } from 'rxjs' | 3 | import { Observable, ReplaySubject } from 'rxjs' |
4 | import { RestExtractor } from '../rest/rest-extractor.service' | 4 | import { RestExtractor } from '../rest/rest-extractor.service' |
5 | import { HttpClient } from '@angular/common/http' | 5 | import { HttpClient, HttpParams } from '@angular/common/http' |
6 | import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos' | 6 | import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos' |
7 | import { AccountService } from '../account/account.service' | 7 | import { AccountService } from '../account/account.service' |
8 | import { ResultList } from '../../../../../shared' | 8 | import { ResultList } from '../../../../../shared' |
@@ -10,6 +10,8 @@ import { VideoChannel } from './video-channel.model' | |||
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { Account } from '@app/shared/account/account.model' | 11 | import { Account } from '@app/shared/account/account.model' |
12 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 12 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
13 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | ||
14 | import { RestService } from '@app/shared/rest' | ||
13 | 15 | ||
14 | @Injectable() | 16 | @Injectable() |
15 | export class VideoChannelService { | 17 | export class VideoChannelService { |
@@ -29,6 +31,7 @@ export class VideoChannelService { | |||
29 | 31 | ||
30 | constructor ( | 32 | constructor ( |
31 | private authHttp: HttpClient, | 33 | private authHttp: HttpClient, |
34 | private restService: RestService, | ||
32 | private restExtractor: RestExtractor | 35 | private restExtractor: RestExtractor |
33 | ) { } | 36 | ) { } |
34 | 37 | ||
@@ -41,8 +44,16 @@ export class VideoChannelService { | |||
41 | ) | 44 | ) |
42 | } | 45 | } |
43 | 46 | ||
44 | listAccountVideoChannels (account: Account): Observable<ResultList<VideoChannel>> { | 47 | listAccountVideoChannels (account: Account, componentPagination?: ComponentPagination): Observable<ResultList<VideoChannel>> { |
45 | return this.authHttp.get<ResultList<VideoChannelServer>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels') | 48 | const pagination = componentPagination |
49 | ? this.restService.componentPaginationToRestPagination(componentPagination) | ||
50 | : { start: 0, count: 20 } | ||
51 | |||
52 | let params = new HttpParams() | ||
53 | params = this.restService.addRestGetParams(params, pagination) | ||
54 | |||
55 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' | ||
56 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) | ||
46 | .pipe( | 57 | .pipe( |
47 | map(res => VideoChannelService.extractVideoChannels(res)), | 58 | map(res => VideoChannelService.extractVideoChannels(res)), |
48 | catchError(err => this.restExtractor.handleError(err)) | 59 | catchError(err => this.restExtractor.handleError(err)) |
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index 268677977..11cf1bd92 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html | |||
@@ -6,7 +6,7 @@ | |||
6 | </div> | 6 | </div> |
7 | </div> | 7 | </div> |
8 | 8 | ||
9 | <my-feed [syndicationItems]="syndicationItems"></my-feed> | 9 | <my-feed *ngIf="titlePage" [syndicationItems]="syndicationItems"></my-feed> |
10 | 10 | ||
11 | <div class="moderation-block" *ngIf="displayModerationBlock"> | 11 | <div class="moderation-block" *ngIf="displayModerationBlock"> |
12 | <my-peertube-checkbox | 12 | <my-peertube-checkbox |
@@ -22,11 +22,18 @@ | |||
22 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" | 22 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" |
23 | class="videos" | 23 | class="videos" |
24 | > | 24 | > |
25 | <my-video-miniature | 25 | <ng-container *ngFor="let video of videos; trackBy: videoById;"> |
26 | *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType" | 26 | <div class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> |
27 | [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" | 27 | {{ getCurrentGroupedDateLabel(video) }} |
28 | (videoBlacklisted)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" | 28 | </div> |
29 | > | 29 | |
30 | </my-video-miniature> | 30 | |
31 | <my-video-miniature | ||
32 | [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType" | ||
33 | [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" | ||
34 | (videoBlacklisted)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" | ||
35 | > | ||
36 | </my-video-miniature> | ||
37 | </ng-container> | ||
31 | </div> | 38 | </div> |
32 | </div> | 39 | </div> |
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss index 9d481d6e4..98b80fdfd 100644 --- a/client/src/app/shared/video/abstract-video-list.scss +++ b/client/src/app/shared/video/abstract-video-list.scss | |||
@@ -24,33 +24,19 @@ | |||
24 | } | 24 | } |
25 | } | 25 | } |
26 | 26 | ||
27 | .margin-content { | 27 | .date-title { |
28 | width: $video-miniature-width * 6; | 28 | font-size: 16px; |
29 | margin: auto !important; | 29 | font-weight: $font-semibold; |
30 | 30 | margin-bottom: 20px; | |
31 | @media screen and (max-width: 1800px) { | 31 | margin-top: -10px; |
32 | width: $video-miniature-width * 5; | 32 | padding-top: 20px; |
33 | } | ||
34 | 33 | ||
35 | @media screen and (max-width: 1800px - $video-miniature-width) { | 34 | &:not(:first-child) { |
36 | width: $video-miniature-width * 4; | 35 | border-top: 1px solid $separator-border-color; |
37 | } | 36 | } |
37 | } | ||
38 | 38 | ||
39 | @media screen and (max-width: 1800px - (2* $video-miniature-width)) { | 39 | .margin-content { |
40 | width: $video-miniature-width * 3; | 40 | @include adapt-margin-content-width; |
41 | } | ||
42 | |||
43 | @media screen and (max-width: 1800px - (3* $video-miniature-width)) { | ||
44 | width: $video-miniature-width * 2; | ||
45 | } | ||
46 | |||
47 | @media screen and (max-width: 500px) { | ||
48 | width: auto; | ||
49 | margin: 0 !important; | ||
50 | |||
51 | .videos { | ||
52 | @include video-miniature-small-screen; | ||
53 | } | ||
54 | } | ||
55 | } | 41 | } |
56 | 42 | ||
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index fa9d38735..eba05c07d 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -11,6 +11,17 @@ import { MiniatureDisplayOptions, OwnerDisplayType } from '@app/shared/video/vid | |||
11 | import { Syndication } from '@app/shared/video/syndication.model' | 11 | import { Syndication } from '@app/shared/video/syndication.model' |
12 | import { Notifier, ServerService } from '@app/core' | 12 | import { Notifier, ServerService } from '@app/core' |
13 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 13 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
14 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
15 | import { isThisMonth, isThisWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' | ||
16 | |||
17 | enum GroupDate { | ||
18 | UNKNOWN = 0, | ||
19 | TODAY = 1, | ||
20 | YESTERDAY = 2, | ||
21 | THIS_WEEK = 3, | ||
22 | THIS_MONTH = 4, | ||
23 | OLDER = 5 | ||
24 | } | ||
14 | 25 | ||
15 | export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook { | 26 | export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook { |
16 | pagination: ComponentPagination = { | 27 | pagination: ComponentPagination = { |
@@ -31,6 +42,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
31 | displayModerationBlock = false | 42 | displayModerationBlock = false |
32 | titleTooltip: string | 43 | titleTooltip: string |
33 | displayVideoActions = true | 44 | displayVideoActions = true |
45 | groupByDate = false | ||
34 | 46 | ||
35 | disabled = false | 47 | disabled = false |
36 | 48 | ||
@@ -50,11 +62,15 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
50 | protected abstract serverService: ServerService | 62 | protected abstract serverService: ServerService |
51 | protected abstract screenService: ScreenService | 63 | protected abstract screenService: ScreenService |
52 | protected abstract router: Router | 64 | protected abstract router: Router |
65 | protected abstract i18n: I18n | ||
53 | abstract titlePage: string | 66 | abstract titlePage: string |
54 | 67 | ||
55 | private resizeSubscription: Subscription | 68 | private resizeSubscription: Subscription |
56 | private angularState: number | 69 | private angularState: number |
57 | 70 | ||
71 | private groupedDateLabels: { [id in GroupDate]: string } | ||
72 | private groupedDates: { [id: number]: GroupDate } = {} | ||
73 | |||
58 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }> | 74 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }> |
59 | 75 | ||
60 | abstract generateSyndicationList (): void | 76 | abstract generateSyndicationList (): void |
@@ -64,6 +80,15 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
64 | } | 80 | } |
65 | 81 | ||
66 | ngOnInit () { | 82 | ngOnInit () { |
83 | this.groupedDateLabels = { | ||
84 | [GroupDate.UNKNOWN]: null, | ||
85 | [GroupDate.TODAY]: this.i18n('Today'), | ||
86 | [GroupDate.YESTERDAY]: this.i18n('Yesterday'), | ||
87 | [GroupDate.THIS_WEEK]: this.i18n('This week'), | ||
88 | [GroupDate.THIS_MONTH]: this.i18n('This month'), | ||
89 | [GroupDate.OLDER]: this.i18n('Older') | ||
90 | } | ||
91 | |||
67 | // Subscribe to route changes | 92 | // Subscribe to route changes |
68 | const routeParams = this.route.snapshot.queryParams | 93 | const routeParams = this.route.snapshot.queryParams |
69 | this.loadRouteParams(routeParams) | 94 | this.loadRouteParams(routeParams) |
@@ -113,6 +138,8 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
113 | this.pagination.totalItems = totalVideos | 138 | this.pagination.totalItems = totalVideos |
114 | this.videos = this.videos.concat(videos) | 139 | this.videos = this.videos.concat(videos) |
115 | 140 | ||
141 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
142 | |||
116 | this.onMoreVideos() | 143 | this.onMoreVideos() |
117 | }, | 144 | }, |
118 | 145 | ||
@@ -134,6 +161,49 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
134 | this.videos = this.videos.filter(v => v.id !== video.id) | 161 | this.videos = this.videos.filter(v => v.id !== video.id) |
135 | } | 162 | } |
136 | 163 | ||
164 | buildGroupedDateLabels () { | ||
165 | let currentGroupedDate: GroupDate = GroupDate.UNKNOWN | ||
166 | |||
167 | for (const video of this.videos) { | ||
168 | const publishedDate = video.publishedAt | ||
169 | |||
170 | if (currentGroupedDate < GroupDate.TODAY && isToday(publishedDate)) { | ||
171 | currentGroupedDate = GroupDate.TODAY | ||
172 | this.groupedDates[ video.id ] = currentGroupedDate | ||
173 | continue | ||
174 | } | ||
175 | |||
176 | if (currentGroupedDate < GroupDate.YESTERDAY && isYesterday(publishedDate)) { | ||
177 | currentGroupedDate = GroupDate.YESTERDAY | ||
178 | this.groupedDates[ video.id ] = currentGroupedDate | ||
179 | continue | ||
180 | } | ||
181 | |||
182 | if (currentGroupedDate < GroupDate.THIS_WEEK && isThisWeek(publishedDate)) { | ||
183 | currentGroupedDate = GroupDate.THIS_WEEK | ||
184 | this.groupedDates[ video.id ] = currentGroupedDate | ||
185 | continue | ||
186 | } | ||
187 | |||
188 | if (currentGroupedDate < GroupDate.THIS_MONTH && isThisMonth(publishedDate)) { | ||
189 | currentGroupedDate = GroupDate.THIS_MONTH | ||
190 | this.groupedDates[ video.id ] = currentGroupedDate | ||
191 | continue | ||
192 | } | ||
193 | |||
194 | if (currentGroupedDate < GroupDate.OLDER) { | ||
195 | currentGroupedDate = GroupDate.OLDER | ||
196 | this.groupedDates[ video.id ] = currentGroupedDate | ||
197 | } | ||
198 | } | ||
199 | } | ||
200 | |||
201 | getCurrentGroupedDateLabel (video: Video) { | ||
202 | if (this.groupByDate === false) return undefined | ||
203 | |||
204 | return this.groupedDateLabels[this.groupedDates[video.id]] | ||
205 | } | ||
206 | |||
137 | // On videos hook for children that want to do something | 207 | // On videos hook for children that want to do something |
138 | protected onMoreVideos () { /* empty */ } | 208 | protected onMoreVideos () { /* empty */ } |
139 | 209 | ||
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts index d6d10d29e..a07560f87 100644 --- a/client/src/app/shared/video/modals/video-download.component.ts +++ b/client/src/app/shared/video/modals/video-download.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, ElementRef, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, ViewChild } from '@angular/core' |
2 | import { VideoDetails } from '../../../shared/video/video-details.model' | 2 | import { VideoDetails } from '../../../shared/video/video-details.model' |
3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 3 | import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { Notifier } from '@app/core' | 5 | import { Notifier } from '@app/core' |
6 | 6 | ||
@@ -16,6 +16,7 @@ export class VideoDownloadComponent { | |||
16 | resolutionId: number | string = -1 | 16 | resolutionId: number | string = -1 |
17 | 17 | ||
18 | video: VideoDetails | 18 | video: VideoDetails |
19 | activeModal: NgbActiveModal | ||
19 | 20 | ||
20 | constructor ( | 21 | constructor ( |
21 | private notifier: Notifier, | 22 | private notifier: Notifier, |
@@ -26,9 +27,7 @@ export class VideoDownloadComponent { | |||
26 | show (video: VideoDetails) { | 27 | show (video: VideoDetails) { |
27 | this.video = video | 28 | this.video = video |
28 | 29 | ||
29 | const m = this.modalService.open(this.modal) | 30 | this.activeModal = this.modalService.open(this.modal) |
30 | m.result.then(() => this.onClose()) | ||
31 | .catch(() => this.onClose()) | ||
32 | 31 | ||
33 | this.resolutionId = this.video.files[0].resolution.id | 32 | this.resolutionId = this.video.files[0].resolution.id |
34 | } | 33 | } |
@@ -39,6 +38,7 @@ export class VideoDownloadComponent { | |||
39 | 38 | ||
40 | download () { | 39 | download () { |
41 | window.location.assign(this.getLink()) | 40 | window.location.assign(this.getLink()) |
41 | this.activeModal.close() | ||
42 | } | 42 | } |
43 | 43 | ||
44 | getLink () { | 44 | getLink () { |
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 22f024656..e4d443a06 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { UserRight, VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared' | 1 | import { VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared' |
2 | import { AuthUser } from '../../core' | ||
3 | import { Video } from '../../shared/video/video.model' | 2 | import { Video } from '../../shared/video/video.model' |
4 | import { Account } from '@app/shared/account/account.model' | 3 | import { Account } from '@app/shared/account/account.model' |
5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 4 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index 1f633d427..67d8e7711 100644 --- a/client/src/app/shared/video/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts | |||
@@ -85,6 +85,11 @@ export class VideoEdit implements VideoUpdate { | |||
85 | const originallyPublishedAt = new Date(values['originallyPublishedAt']) | 85 | const originallyPublishedAt = new Date(values['originallyPublishedAt']) |
86 | this.originallyPublishedAt = originallyPublishedAt.toISOString() | 86 | this.originallyPublishedAt = originallyPublishedAt.toISOString() |
87 | } | 87 | } |
88 | |||
89 | // Use the same file than the preview for the thumbnail | ||
90 | if (this.previewfile) { | ||
91 | this.thumbnailfile = this.previewfile | ||
92 | } | ||
88 | } | 93 | } |
89 | 94 | ||
90 | toFormPatch () { | 95 | toFormPatch () { |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 0cef3eb8f..6f9de9241 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -52,7 +52,6 @@ export class Video implements VideoServerModel { | |||
52 | 52 | ||
53 | account: { | 53 | account: { |
54 | id: number | 54 | id: number |
55 | uuid: string | ||
56 | name: string | 55 | name: string |
57 | displayName: string | 56 | displayName: string |
58 | url: string | 57 | url: string |
@@ -62,7 +61,6 @@ export class Video implements VideoServerModel { | |||
62 | 61 | ||
63 | channel: { | 62 | channel: { |
64 | id: number | 63 | id: number |
65 | uuid: string | ||
66 | name: string | 64 | name: string |
67 | displayName: string | 65 | displayName: string |
68 | url: string | 66 | url: string |
diff --git a/client/src/app/shared/video/videos-selection.component.ts b/client/src/app/shared/video/videos-selection.component.ts index 955ebca9f..d69f7b70e 100644 --- a/client/src/app/shared/video/videos-selection.component.ts +++ b/client/src/app/shared/video/videos-selection.component.ts | |||
@@ -20,6 +20,7 @@ import { Video } from '@app/shared/video/video.model' | |||
20 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' | 20 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' |
21 | import { VideoSortField } from '@app/shared/video/sort-field.type' | 21 | import { VideoSortField } from '@app/shared/video/sort-field.type' |
22 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 22 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
23 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
23 | 24 | ||
24 | export type SelectionType = { [ id: number ]: boolean } | 25 | export type SelectionType = { [ id: number ]: boolean } |
25 | 26 | ||
@@ -44,6 +45,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni | |||
44 | globalButtonsTemplate: TemplateRef<any> | 45 | globalButtonsTemplate: TemplateRef<any> |
45 | 46 | ||
46 | constructor ( | 47 | constructor ( |
48 | protected i18n: I18n, | ||
47 | protected router: Router, | 49 | protected router: Router, |
48 | protected route: ActivatedRoute, | 50 | protected route: ActivatedRoute, |
49 | protected notifier: Notifier, | 51 | protected notifier: Notifier, |
diff --git a/client/src/app/signup/index.ts b/client/src/app/signup/index.ts deleted file mode 100644 index b0aca9723..000000000 --- a/client/src/app/signup/index.ts +++ /dev/null | |||
@@ -1,3 +0,0 @@ | |||
1 | export * from './signup-routing.module' | ||
2 | export * from './signup.component' | ||
3 | export * from './signup.module' | ||
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html deleted file mode 100644 index 07d24b381..000000000 --- a/client/src/app/signup/signup.component.html +++ /dev/null | |||
@@ -1,72 +0,0 @@ | |||
1 | <div class="margin-content"> | ||
2 | |||
3 | <div i18n class="title-page title-page-single"> | ||
4 | Create an account | ||
5 | </div> | ||
6 | |||
7 | <div *ngIf="info" class="alert alert-info">{{ info }}</div> | ||
8 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
9 | |||
10 | <div class="d-flex justify-content-left flex-wrap"> | ||
11 | <form role="form" (ngSubmit)="signup()" [formGroup]="form"> | ||
12 | <div class="form-group"> | ||
13 | <label for="username" i18n>Username</label> | ||
14 | |||
15 | <div class="input-group"> | ||
16 | <input | ||
17 | type="text" id="username" i18n-placeholder placeholder="Example: jane_doe" | ||
18 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" | ||
19 | > | ||
20 | <div class="input-group-append"> | ||
21 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
22 | </div> | ||
23 | </div> | ||
24 | |||
25 | <div *ngIf="formErrors.username" class="form-error"> | ||
26 | {{ formErrors.username }} | ||
27 | </div> | ||
28 | </div> | ||
29 | |||
30 | <div class="form-group"> | ||
31 | <label for="email" i18n>Email</label> | ||
32 | <input | ||
33 | type="text" id="email" i18n-placeholder placeholder="Email" | ||
34 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" | ||
35 | > | ||
36 | <div *ngIf="formErrors.email" class="form-error"> | ||
37 | {{ formErrors.email }} | ||
38 | </div> | ||
39 | </div> | ||
40 | |||
41 | <div class="form-group"> | ||
42 | <label for="password" i18n>Password</label> | ||
43 | <input | ||
44 | type="password" id="password" i18n-placeholder placeholder="Password" | ||
45 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
46 | > | ||
47 | <div *ngIf="formErrors.password" class="form-error"> | ||
48 | {{ formErrors.password }} | ||
49 | </div> | ||
50 | </div> | ||
51 | |||
52 | <div class="form-group form-group-terms"> | ||
53 | <my-peertube-checkbox | ||
54 | inputName="terms" formControlName="terms" | ||
55 | i18n-labelHtml labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" | ||
56 | ></my-peertube-checkbox> | ||
57 | |||
58 | <div *ngIf="formErrors.terms" class="form-error"> | ||
59 | {{ formErrors.terms }} | ||
60 | </div> | ||
61 | </div> | ||
62 | |||
63 | <input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone"> | ||
64 | </form> | ||
65 | |||
66 | <div> | ||
67 | <label i18n>Features found on this instance</label> | ||
68 | <my-instance-features-table></my-instance-features-table> | ||
69 | </div> | ||
70 | </div> | ||
71 | |||
72 | </div> | ||
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts deleted file mode 100644 index 13941ec79..000000000 --- a/client/src/app/signup/signup.component.ts +++ /dev/null | |||
@@ -1,78 +0,0 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' | ||
3 | import { UserCreate } from '../../../../shared' | ||
4 | import { FormReactive, UserService, UserValidatorsService } from '../shared' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-signup', | ||
10 | templateUrl: './signup.component.html', | ||
11 | styleUrls: [ './signup.component.scss' ] | ||
12 | }) | ||
13 | export class SignupComponent extends FormReactive implements OnInit { | ||
14 | info: string = null | ||
15 | error: string = null | ||
16 | signupDone = false | ||
17 | |||
18 | constructor ( | ||
19 | protected formValidatorService: FormValidatorService, | ||
20 | private authService: AuthService, | ||
21 | private userValidatorsService: UserValidatorsService, | ||
22 | private notifier: Notifier, | ||
23 | private userService: UserService, | ||
24 | private serverService: ServerService, | ||
25 | private redirectService: RedirectService, | ||
26 | private i18n: I18n | ||
27 | ) { | ||
28 | super() | ||
29 | } | ||
30 | |||
31 | get instanceHost () { | ||
32 | return window.location.host | ||
33 | } | ||
34 | |||
35 | get requiresEmailVerification () { | ||
36 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
37 | } | ||
38 | |||
39 | ngOnInit () { | ||
40 | this.buildForm({ | ||
41 | username: this.userValidatorsService.USER_USERNAME, | ||
42 | password: this.userValidatorsService.USER_PASSWORD, | ||
43 | email: this.userValidatorsService.USER_EMAIL, | ||
44 | terms: this.userValidatorsService.USER_TERMS | ||
45 | }) | ||
46 | } | ||
47 | |||
48 | signup () { | ||
49 | this.error = null | ||
50 | |||
51 | const userCreate: UserCreate = this.form.value | ||
52 | |||
53 | this.userService.signup(userCreate).subscribe( | ||
54 | () => { | ||
55 | this.signupDone = true | ||
56 | |||
57 | if (this.requiresEmailVerification) { | ||
58 | this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.') | ||
59 | return | ||
60 | } | ||
61 | |||
62 | // Auto login | ||
63 | this.authService.login(userCreate.username, userCreate.password) | ||
64 | .subscribe( | ||
65 | () => { | ||
66 | this.notifier.success(this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })) | ||
67 | |||
68 | this.redirectService.redirectToHomepage() | ||
69 | }, | ||
70 | |||
71 | err => this.error = err.message | ||
72 | ) | ||
73 | }, | ||
74 | |||
75 | err => this.error = err.message | ||
76 | ) | ||
77 | } | ||
78 | } | ||
diff --git a/client/src/app/signup/signup.module.ts b/client/src/app/signup/signup.module.ts deleted file mode 100644 index 61560ddcf..000000000 --- a/client/src/app/signup/signup.module.ts +++ /dev/null | |||
@@ -1,24 +0,0 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | |||
3 | import { SignupRoutingModule } from './signup-routing.module' | ||
4 | import { SignupComponent } from './signup.component' | ||
5 | import { SharedModule } from '../shared' | ||
6 | |||
7 | @NgModule({ | ||
8 | imports: [ | ||
9 | SignupRoutingModule, | ||
10 | SharedModule | ||
11 | ], | ||
12 | |||
13 | declarations: [ | ||
14 | SignupComponent | ||
15 | ], | ||
16 | |||
17 | exports: [ | ||
18 | SignupComponent | ||
19 | ], | ||
20 | |||
21 | providers: [ | ||
22 | ] | ||
23 | }) | ||
24 | export class SignupModule { } | ||
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 99695204d..28572d611 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 | |||
@@ -187,18 +187,14 @@ | |||
187 | <ng-template ngbTabContent> | 187 | <ng-template ngbTabContent> |
188 | <div class="row advanced-settings"> | 188 | <div class="row advanced-settings"> |
189 | <div class="col-md-12 col-xl-8"> | 189 | <div class="col-md-12 col-xl-8"> |
190 | <div class="form-group"> | ||
191 | <my-image-upload | ||
192 | i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile" | ||
193 | previewWidth="200px" previewHeight="110px" | ||
194 | ></my-image-upload> | ||
195 | </div> | ||
196 | 190 | ||
197 | <div class="form-group"> | 191 | <div class="form-group"> |
198 | <my-image-upload | 192 | <label i18n for="previewfile">Video preview</label> |
199 | i18n-inputLabel inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile" | 193 | |
194 | <my-preview-upload | ||
195 | i18n-inputLabel inputLabel="Edit" inputName="previewfile" formControlName="previewfile" | ||
200 | previewWidth="360px" previewHeight="200px" | 196 | previewWidth="360px" previewHeight="200px" |
201 | ></my-image-upload> | 197 | ></my-preview-upload> |
202 | </div> | 198 | </div> |
203 | 199 | ||
204 | <div class="form-group"> | 200 | <div class="form-group"> |
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 c80efd802..95d397b52 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 | |||
@@ -100,7 +100,6 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
100 | language: this.videoValidatorsService.VIDEO_LANGUAGE, | 100 | language: this.videoValidatorsService.VIDEO_LANGUAGE, |
101 | description: this.videoValidatorsService.VIDEO_DESCRIPTION, | 101 | description: this.videoValidatorsService.VIDEO_DESCRIPTION, |
102 | tags: null, | 102 | tags: null, |
103 | thumbnailfile: null, | ||
104 | previewfile: null, | 103 | previewfile: null, |
105 | support: this.videoValidatorsService.VIDEO_SUPPORT, | 104 | support: this.videoValidatorsService.VIDEO_SUPPORT, |
106 | schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, | 105 | schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, |
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 536769d2f..3247a2bd6 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 | |||
@@ -26,6 +26,27 @@ | |||
26 | </select> | 26 | </select> |
27 | </div> | 27 | </div> |
28 | </div> | 28 | </div> |
29 | |||
30 | <ng-container *ngIf="isUploadingAudioFile"> | ||
31 | <div class="form-group audio-preview"> | ||
32 | <label i18n for="previewfileUpload">Video background image</label> | ||
33 | |||
34 | <div i18n class="audio-image-info"> | ||
35 | Image that will be merged with your audio file. | ||
36 | <br /> | ||
37 | The chosen image will be definitive and cannot be modified. | ||
38 | </div> | ||
39 | |||
40 | <my-preview-upload | ||
41 | i18n-inputLabel inputLabel="Edit" inputName="previewfileUpload" [(ngModel)]="previewfileUpload" | ||
42 | previewWidth="360px" previewHeight="200px" | ||
43 | ></my-preview-upload> | ||
44 | </div> | ||
45 | |||
46 | <div class="form-group upload-audio-button"> | ||
47 | <my-button className="orange-button" i18n-label [label]="getAudioUploadLabel()" icon="upload" (click)="uploadFirstStep(true)"></my-button> | ||
48 | </div> | ||
49 | </ng-container> | ||
29 | </div> | 50 | </div> |
30 | </div> | 51 | </div> |
31 | 52 | ||
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 8adf8f169..684342f09 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 | |||
@@ -1,9 +1,20 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | .first-step-block .form-group-channel { | 4 | .first-step-block { |
5 | margin-bottom: 20px; | 5 | |
6 | margin-top: 35px; | 6 | .form-group-channel { |
7 | margin-bottom: 20px; | ||
8 | margin-top: 35px; | ||
9 | } | ||
10 | |||
11 | .audio-image-info { | ||
12 | margin-bottom: 10px; | ||
13 | } | ||
14 | |||
15 | .audio-preview { | ||
16 | margin: 30px 0; | ||
17 | } | ||
7 | } | 18 | } |
8 | 19 | ||
9 | .upload-progress-cancel { | 20 | .upload-progress-cancel { |
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 d6d4bad21..73de25c59 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 | |||
@@ -35,8 +35,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
35 | userVideoQuotaUsed = 0 | 35 | userVideoQuotaUsed = 0 |
36 | userVideoQuotaUsedDaily = 0 | 36 | userVideoQuotaUsedDaily = 0 |
37 | 37 | ||
38 | isUploadingAudioFile = false | ||
38 | isUploadingVideo = false | 39 | isUploadingVideo = false |
39 | isUpdatingVideo = false | 40 | isUpdatingVideo = false |
41 | |||
40 | videoUploaded = false | 42 | videoUploaded = false |
41 | videoUploadObservable: Subscription = null | 43 | videoUploadObservable: Subscription = null |
42 | videoUploadPercents = 0 | 44 | videoUploadPercents = 0 |
@@ -44,7 +46,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
44 | id: 0, | 46 | id: 0, |
45 | uuid: '' | 47 | uuid: '' |
46 | } | 48 | } |
49 | |||
47 | waitTranscodingEnabled = true | 50 | waitTranscodingEnabled = true |
51 | previewfileUpload: File | ||
48 | 52 | ||
49 | error: string | 53 | error: string |
50 | 54 | ||
@@ -100,6 +104,17 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
100 | } | 104 | } |
101 | } | 105 | } |
102 | 106 | ||
107 | getVideoFile () { | ||
108 | return this.videofileInput.nativeElement.files[0] | ||
109 | } | ||
110 | |||
111 | getAudioUploadLabel () { | ||
112 | const videofile = this.getVideoFile() | ||
113 | if (!videofile) return this.i18n('Upload') | ||
114 | |||
115 | return this.i18n('Upload {{videofileName}}', { videofileName: videofile.name }) | ||
116 | } | ||
117 | |||
103 | fileChange () { | 118 | fileChange () { |
104 | this.uploadFirstStep() | 119 | this.uploadFirstStep() |
105 | } | 120 | } |
@@ -114,38 +129,15 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
114 | } | 129 | } |
115 | } | 130 | } |
116 | 131 | ||
117 | uploadFirstStep () { | 132 | uploadFirstStep (clickedOnButton = false) { |
118 | const videofile = this.videofileInput.nativeElement.files[0] | 133 | const videofile = this.getVideoFile() |
119 | if (!videofile) return | 134 | if (!videofile) return |
120 | 135 | ||
121 | // Check global user quota | 136 | if (!this.checkGlobalUserQuota(videofile)) return |
122 | const bytePipes = new BytesPipe() | 137 | if (!this.checkDailyUserQuota(videofile)) return |
123 | const videoQuota = this.authService.getUser().videoQuota | ||
124 | if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { | ||
125 | const msg = this.i18n( | ||
126 | 'Your video quota is exceeded with this video (video size: {{videoSize}}, used: {{videoQuotaUsed}}, quota: {{videoQuota}})', | ||
127 | { | ||
128 | videoSize: bytePipes.transform(videofile.size, 0), | ||
129 | videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0), | ||
130 | videoQuota: bytePipes.transform(videoQuota, 0) | ||
131 | } | ||
132 | ) | ||
133 | this.notifier.error(msg) | ||
134 | return | ||
135 | } | ||
136 | 138 | ||
137 | // Check daily user quota | 139 | if (clickedOnButton === false && this.isAudioFile(videofile.name)) { |
138 | const videoQuotaDaily = this.authService.getUser().videoQuotaDaily | 140 | this.isUploadingAudioFile = true |
139 | if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { | ||
140 | const msg = this.i18n( | ||
141 | 'Your daily video quota is exceeded with this video (video size: {{videoSize}}, used: {{quotaUsedDaily}}, quota: {{quotaDaily}})', | ||
142 | { | ||
143 | videoSize: bytePipes.transform(videofile.size, 0), | ||
144 | quotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0), | ||
145 | quotaDaily: bytePipes.transform(videoQuotaDaily, 0) | ||
146 | } | ||
147 | ) | ||
148 | this.notifier.error(msg) | ||
149 | return | 141 | return |
150 | } | 142 | } |
151 | 143 | ||
@@ -180,6 +172,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
180 | formData.append('channelId', '' + channelId) | 172 | formData.append('channelId', '' + channelId) |
181 | formData.append('videofile', videofile) | 173 | formData.append('videofile', videofile) |
182 | 174 | ||
175 | if (this.previewfileUpload) { | ||
176 | formData.append('previewfile', this.previewfileUpload) | ||
177 | formData.append('thumbnailfile', this.previewfileUpload) | ||
178 | } | ||
179 | |||
183 | this.isUploadingVideo = true | 180 | this.isUploadingVideo = true |
184 | this.firstStepDone.emit(name) | 181 | this.firstStepDone.emit(name) |
185 | 182 | ||
@@ -187,7 +184,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
187 | name, | 184 | name, |
188 | privacy, | 185 | privacy, |
189 | nsfw, | 186 | nsfw, |
190 | channelId | 187 | channelId, |
188 | previewfile: this.previewfileUpload | ||
191 | }) | 189 | }) |
192 | 190 | ||
193 | this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) | 191 | this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) |
@@ -251,4 +249,52 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
251 | } | 249 | } |
252 | ) | 250 | ) |
253 | } | 251 | } |
252 | |||
253 | private checkGlobalUserQuota (videofile: File) { | ||
254 | const bytePipes = new BytesPipe() | ||
255 | |||
256 | // Check global user quota | ||
257 | const videoQuota = this.authService.getUser().videoQuota | ||
258 | if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { | ||
259 | const msg = this.i18n( | ||
260 | 'Your video quota is exceeded with this video (video size: {{videoSize}}, used: {{videoQuotaUsed}}, quota: {{videoQuota}})', | ||
261 | { | ||
262 | videoSize: bytePipes.transform(videofile.size, 0), | ||
263 | videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0), | ||
264 | videoQuota: bytePipes.transform(videoQuota, 0) | ||
265 | } | ||
266 | ) | ||
267 | this.notifier.error(msg) | ||
268 | |||
269 | return false | ||
270 | } | ||
271 | |||
272 | return true | ||
273 | } | ||
274 | |||
275 | private checkDailyUserQuota (videofile: File) { | ||
276 | const bytePipes = new BytesPipe() | ||
277 | |||
278 | // Check daily user quota | ||
279 | const videoQuotaDaily = this.authService.getUser().videoQuotaDaily | ||
280 | if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { | ||
281 | const msg = this.i18n( | ||
282 | 'Your daily video quota is exceeded with this video (video size: {{videoSize}}, used: {{quotaUsedDaily}}, quota: {{quotaDaily}})', | ||
283 | { | ||
284 | videoSize: bytePipes.transform(videofile.size, 0), | ||
285 | quotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0), | ||
286 | quotaDaily: bytePipes.transform(videoQuotaDaily, 0) | ||
287 | } | ||
288 | ) | ||
289 | this.notifier.error(msg) | ||
290 | |||
291 | return false | ||
292 | } | ||
293 | |||
294 | return true | ||
295 | } | ||
296 | |||
297 | private isAudioFile (filename: string) { | ||
298 | return filename.endsWith('.mp3') || filename.endsWith('.flac') || filename.endsWith('.ogg') | ||
299 | } | ||
254 | } | 300 | } |
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 631504eab..2d13f1b58 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -6,7 +6,7 @@ import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | |||
6 | import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' | 6 | import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' |
7 | import { MetaService } from '@ngx-meta/core' | 7 | import { MetaService } from '@ngx-meta/core' |
8 | import { Notifier, ServerService } from '@app/core' | 8 | import { Notifier, ServerService } from '@app/core' |
9 | import { forkJoin, Subscription } from 'rxjs' | 9 | import { forkJoin, Observable, Subscription } from 'rxjs' |
10 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 10 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
11 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' | 11 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' |
12 | import { AuthService, ConfirmService } from '../../core' | 12 | import { AuthService, ConfirmService } from '../../core' |
@@ -135,22 +135,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
135 | 135 | ||
136 | setLike () { | 136 | setLike () { |
137 | if (this.isUserLoggedIn() === false) return | 137 | if (this.isUserLoggedIn() === false) return |
138 | if (this.userRating === 'like') { | 138 | |
139 | // Already liked this video | 139 | // Already liked this video |
140 | this.setRating('none') | 140 | if (this.userRating === 'like') this.setRating('none') |
141 | } else { | 141 | else this.setRating('like') |
142 | this.setRating('like') | ||
143 | } | ||
144 | } | 142 | } |
145 | 143 | ||
146 | setDislike () { | 144 | setDislike () { |
147 | if (this.isUserLoggedIn() === false) return | 145 | if (this.isUserLoggedIn() === false) return |
148 | if (this.userRating === 'dislike') { | 146 | |
149 | // Already disliked this video | 147 | // Already disliked this video |
150 | this.setRating('none') | 148 | if (this.userRating === 'dislike') this.setRating('none') |
151 | } else { | 149 | else this.setRating('dislike') |
152 | this.setRating('dislike') | ||
153 | } | ||
154 | } | 150 | } |
155 | 151 | ||
156 | showMoreDescription () { | 152 | showMoreDescription () { |
@@ -249,12 +245,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
249 | ) | 245 | ) |
250 | .subscribe(([ video, captionsResult ]) => { | 246 | .subscribe(([ video, captionsResult ]) => { |
251 | const queryParams = this.route.snapshot.queryParams | 247 | const queryParams = this.route.snapshot.queryParams |
252 | const startTime = queryParams.start | ||
253 | const stopTime = queryParams.stop | ||
254 | const subtitle = queryParams.subtitle | ||
255 | const playerMode = queryParams.mode | ||
256 | 248 | ||
257 | this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode }) | 249 | const urlOptions = { |
250 | startTime: queryParams.start, | ||
251 | stopTime: queryParams.stop, | ||
252 | subtitle: queryParams.subtitle, | ||
253 | playerMode: queryParams.mode | ||
254 | } | ||
255 | |||
256 | this.onVideoFetched(video, captionsResult.data, urlOptions) | ||
258 | .catch(err => this.handleError(err)) | 257 | .catch(err => this.handleError(err)) |
259 | }) | 258 | }) |
260 | } | 259 | } |
@@ -279,6 +278,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
279 | private updateVideoDescription (description: string) { | 278 | private updateVideoDescription (description: string) { |
280 | this.video.description = description | 279 | this.video.description = description |
281 | this.setVideoDescriptionHTML() | 280 | this.setVideoDescriptionHTML() |
281 | .catch(err => console.error(err)) | ||
282 | } | 282 | } |
283 | 283 | ||
284 | private async setVideoDescriptionHTML () { | 284 | private async setVideoDescriptionHTML () { |
@@ -385,7 +385,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
385 | captions: videoCaptions.length !== 0, | 385 | captions: videoCaptions.length !== 0, |
386 | peertubeLink: false, | 386 | peertubeLink: false, |
387 | 387 | ||
388 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, | 388 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE |
389 | ? this.videoService.getVideoViewUrl(this.video.uuid) | ||
390 | : null, | ||
389 | embedUrl: this.video.embedUrl, | 391 | embedUrl: this.video.embedUrl, |
390 | 392 | ||
391 | language: this.localeId, | 393 | language: this.localeId, |
@@ -466,20 +468,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
466 | } | 468 | } |
467 | 469 | ||
468 | private setRating (nextRating: UserVideoRateType) { | 470 | private setRating (nextRating: UserVideoRateType) { |
469 | let method | 471 | const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable<any> } = { |
470 | switch (nextRating) { | 472 | like: this.videoService.setVideoLike, |
471 | case 'like': | 473 | dislike: this.videoService.setVideoDislike, |
472 | method = this.videoService.setVideoLike | 474 | none: this.videoService.unsetVideoLike |
473 | break | ||
474 | case 'dislike': | ||
475 | method = this.videoService.setVideoDislike | ||
476 | break | ||
477 | case 'none': | ||
478 | method = this.videoService.unsetVideoLike | ||
479 | break | ||
480 | } | 475 | } |
481 | 476 | ||
482 | method.call(this.videoService, this.video.id) | 477 | ratingMethods[nextRating].call(this.videoService, this.video.id) |
483 | .subscribe( | 478 | .subscribe( |
484 | () => { | 479 | () => { |
485 | // Update the video like attribute | 480 | // Update the video like attribute |
@@ -545,25 +540,29 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
545 | private flushPlayer () { | 540 | private flushPlayer () { |
546 | // Remove player if it exists | 541 | // Remove player if it exists |
547 | if (this.player) { | 542 | if (this.player) { |
548 | this.player.dispose() | 543 | try { |
549 | this.player = undefined | 544 | this.player.dispose() |
545 | this.player = undefined | ||
546 | } catch (err) { | ||
547 | console.error('Cannot dispose player.', err) | ||
548 | } | ||
550 | } | 549 | } |
551 | } | 550 | } |
552 | 551 | ||
553 | private initHotkeys () { | 552 | private initHotkeys () { |
554 | this.hotkeys = [ | 553 | this.hotkeys = [ |
555 | new Hotkey('shift+l', (event: KeyboardEvent): boolean => { | 554 | new Hotkey('shift+l', () => { |
556 | this.setLike() | 555 | this.setLike() |
557 | return false | 556 | return false |
558 | }, undefined, this.i18n('Like the video')), | 557 | }, undefined, this.i18n('Like the video')), |
559 | new Hotkey('shift+d', (event: KeyboardEvent): boolean => { | 558 | |
559 | new Hotkey('shift+d', () => { | ||
560 | this.setDislike() | 560 | this.setDislike() |
561 | return false | 561 | return false |
562 | }, undefined, this.i18n('Dislike the video')), | 562 | }, undefined, this.i18n('Dislike the video')), |
563 | new Hotkey('shift+s', (event: KeyboardEvent): boolean => { | 563 | |
564 | this.subscribeButton.subscribed ? | 564 | new Hotkey('shift+s', () => { |
565 | this.subscribeButton.unsubscribe() : | 565 | this.subscribeButton.subscribed ? this.subscribeButton.unsubscribe() : this.subscribeButton.subscribe() |
566 | this.subscribeButton.subscribe() | ||
567 | return false | 566 | return false |
568 | }, undefined, this.i18n('Subscribe to the account')) | 567 | }, undefined, this.i18n('Subscribe to the account')) |
569 | ] | 568 | ] |
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts index 13d4023c2..65543343c 100644 --- a/client/src/app/videos/video-list/video-local.component.ts +++ b/client/src/app/videos/video-list/video-local.component.ts | |||
@@ -22,13 +22,13 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On | |||
22 | filter: VideoFilter = 'local' | 22 | filter: VideoFilter = 'local' |
23 | 23 | ||
24 | constructor ( | 24 | constructor ( |
25 | protected i18n: I18n, | ||
25 | protected router: Router, | 26 | protected router: Router, |
26 | protected serverService: ServerService, | 27 | protected serverService: ServerService, |
27 | protected route: ActivatedRoute, | 28 | protected route: ActivatedRoute, |
28 | protected notifier: Notifier, | 29 | protected notifier: Notifier, |
29 | protected authService: AuthService, | 30 | protected authService: AuthService, |
30 | protected screenService: ScreenService, | 31 | protected screenService: ScreenService, |
31 | private i18n: I18n, | ||
32 | private videoService: VideoService | 32 | private videoService: VideoService |
33 | ) { | 33 | ) { |
34 | super() | 34 | super() |
diff --git a/client/src/app/videos/video-list/video-overview.component.html b/client/src/app/videos/video-list/video-overview.component.html index b644dd798..f59de584a 100644 --- a/client/src/app/videos/video-list/video-overview.component.html +++ b/client/src/app/videos/video-list/video-overview.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | <div class="no-results" i18n *ngIf="notResults">No results.</div> | 3 | <div class="no-results" i18n *ngIf="notResults">No results.</div> |
4 | 4 | ||
5 | <div class="section" *ngFor="let object of overview.categories"> | 5 | <div class="section" *ngFor="let object of overview.categories"> |
6 | <div class="section-title" i18n> | 6 | <div class="section-title"> |
7 | <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> | 7 | <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> |
8 | </div> | 8 | </div> |
9 | 9 | ||
@@ -11,7 +11,7 @@ | |||
11 | </div> | 11 | </div> |
12 | 12 | ||
13 | <div class="section" *ngFor="let object of overview.tags"> | 13 | <div class="section" *ngFor="let object of overview.tags"> |
14 | <div class="section-title" i18n> | 14 | <div class="section-title"> |
15 | <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> | 15 | <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> |
16 | </div> | 16 | </div> |
17 | 17 | ||
@@ -19,7 +19,7 @@ | |||
19 | </div> | 19 | </div> |
20 | 20 | ||
21 | <div class="section channel" *ngFor="let object of overview.channels"> | 21 | <div class="section channel" *ngFor="let object of overview.channels"> |
22 | <div class="section-title" i18n> | 22 | <div class="section-title"> |
23 | <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> | 23 | <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> |
24 | <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" /> | 24 | <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" /> |
25 | 25 | ||
diff --git a/client/src/app/videos/video-list/video-overview.component.scss b/client/src/app/videos/video-list/video-overview.component.scss index a24766783..ade6f53b7 100644 --- a/client/src/app/videos/video-list/video-overview.component.scss +++ b/client/src/app/videos/video-list/video-overview.component.scss | |||
@@ -2,62 +2,10 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | .section { | 5 | .margin-content { |
6 | max-height: 500px; // 2 rows max | 6 | @include adapt-margin-content-width; |
7 | overflow: hidden; | ||
8 | padding-top: 10px; | ||
9 | |||
10 | &:first-child { | ||
11 | padding-top: 30px; | ||
12 | } | ||
13 | |||
14 | my-video-miniature { | ||
15 | text-align: left; | ||
16 | } | ||
17 | } | ||
18 | |||
19 | .section-title { | ||
20 | font-size: 24px; | ||
21 | font-weight: $font-semibold; | ||
22 | margin-bottom: 10px; | ||
23 | |||
24 | a { | ||
25 | &:hover, &:focus:not(.focus-visible), &:active { | ||
26 | text-decoration: none; | ||
27 | outline: none; | ||
28 | } | ||
29 | |||
30 | color: var(--mainForegroundColor); | ||
31 | } | ||
32 | } | 7 | } |
33 | 8 | ||
34 | .channel { | 9 | .section { |
35 | .section-title a { | 10 | @include miniature-rows; |
36 | display: flex; | ||
37 | width: fit-content; | ||
38 | align-items: center; | ||
39 | |||
40 | img { | ||
41 | @include avatar(28px); | ||
42 | |||
43 | margin-right: 8px; | ||
44 | } | ||
45 | } | ||
46 | } | ||
47 | |||
48 | @media screen and (max-width: 500px) { | ||
49 | .margin-content { | ||
50 | margin: 0 !important; | ||
51 | } | ||
52 | |||
53 | .section-title { | ||
54 | font-size: 17px; | ||
55 | } | ||
56 | |||
57 | .section { | ||
58 | max-height: initial; | ||
59 | overflow: initial; | ||
60 | |||
61 | @include video-miniature-small-screen; | ||
62 | } | ||
63 | } | 11 | } |
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts index 80cef813e..f54bade98 100644 --- a/client/src/app/videos/video-list/video-recently-added.component.ts +++ b/client/src/app/videos/video-list/video-recently-added.component.ts | |||
@@ -17,15 +17,16 @@ import { Notifier, ServerService } from '@app/core' | |||
17 | export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { | 17 | export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { |
18 | titlePage: string | 18 | titlePage: string |
19 | sort: VideoSortField = '-publishedAt' | 19 | sort: VideoSortField = '-publishedAt' |
20 | groupByDate = true | ||
20 | 21 | ||
21 | constructor ( | 22 | constructor ( |
23 | protected i18n: I18n, | ||
22 | protected route: ActivatedRoute, | 24 | protected route: ActivatedRoute, |
23 | protected serverService: ServerService, | 25 | protected serverService: ServerService, |
24 | protected router: Router, | 26 | protected router: Router, |
25 | protected notifier: Notifier, | 27 | protected notifier: Notifier, |
26 | protected authService: AuthService, | 28 | protected authService: AuthService, |
27 | protected screenService: ScreenService, | 29 | protected screenService: ScreenService, |
28 | private i18n: I18n, | ||
29 | private videoService: VideoService | 30 | private videoService: VideoService |
30 | ) { | 31 | ) { |
31 | super() | 32 | super() |
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts index e2ad95bc4..a2c819ebe 100644 --- a/client/src/app/videos/video-list/video-trending.component.ts +++ b/client/src/app/videos/video-list/video-trending.component.ts | |||
@@ -19,13 +19,13 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, | |||
19 | defaultSort: VideoSortField = '-trending' | 19 | defaultSort: VideoSortField = '-trending' |
20 | 20 | ||
21 | constructor ( | 21 | constructor ( |
22 | protected i18n: I18n, | ||
22 | protected router: Router, | 23 | protected router: Router, |
23 | protected serverService: ServerService, | 24 | protected serverService: ServerService, |
24 | protected route: ActivatedRoute, | 25 | protected route: ActivatedRoute, |
25 | protected notifier: Notifier, | 26 | protected notifier: Notifier, |
26 | protected authService: AuthService, | 27 | protected authService: AuthService, |
27 | protected screenService: ScreenService, | 28 | protected screenService: ScreenService, |
28 | private i18n: I18n, | ||
29 | private videoService: VideoService | 29 | private videoService: VideoService |
30 | ) { | 30 | ) { |
31 | super() | 31 | super() |
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts index 2f0685ccc..3caa371d8 100644 --- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts | |||
@@ -19,15 +19,16 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement | |||
19 | titlePage: string | 19 | titlePage: string |
20 | sort = '-publishedAt' as VideoSortField | 20 | sort = '-publishedAt' as VideoSortField |
21 | ownerDisplayType: OwnerDisplayType = 'auto' | 21 | ownerDisplayType: OwnerDisplayType = 'auto' |
22 | groupByDate = true | ||
22 | 23 | ||
23 | constructor ( | 24 | constructor ( |
25 | protected i18n: I18n, | ||
24 | protected router: Router, | 26 | protected router: Router, |
25 | protected serverService: ServerService, | 27 | protected serverService: ServerService, |
26 | protected route: ActivatedRoute, | 28 | protected route: ActivatedRoute, |
27 | protected notifier: Notifier, | 29 | protected notifier: Notifier, |
28 | protected authService: AuthService, | 30 | protected authService: AuthService, |
29 | protected screenService: ScreenService, | 31 | protected screenService: ScreenService, |
30 | private i18n: I18n, | ||
31 | private videoService: VideoService | 32 | private videoService: VideoService |
32 | ) { | 33 | ) { |
33 | super() | 34 | super() |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 6cdd54372..31cbc7dfd 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -117,8 +117,17 @@ export class PeertubePlayerManager { | |||
117 | videojs(options.common.playerElement, videojsOptions, function (this: any) { | 117 | videojs(options.common.playerElement, videojsOptions, function (this: any) { |
118 | const player = this | 118 | const player = this |
119 | 119 | ||
120 | player.tech_.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options)) | 120 | let alreadyFallback = false |
121 | player.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options)) | 121 | |
122 | player.tech_.one('error', () => { | ||
123 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) | ||
124 | alreadyFallback = true | ||
125 | }) | ||
126 | |||
127 | player.one('error', () => { | ||
128 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) | ||
129 | alreadyFallback = true | ||
130 | }) | ||
122 | 131 | ||
123 | self.addContextMenu(mode, player, options.common.embedUrl) | 132 | self.addContextMenu(mode, player, options.common.embedUrl) |
124 | 133 | ||
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss index b62187fd2..0c2ee2d0d 100644 --- a/client/src/sass/include/_miniature.scss +++ b/client/src/sass/include/_miniature.scss | |||
@@ -138,3 +138,100 @@ $play-overlay-width: 18px; | |||
138 | } | 138 | } |
139 | } | 139 | } |
140 | } | 140 | } |
141 | |||
142 | @mixin miniature-rows { | ||
143 | max-height: 540px; // 2 rows max | ||
144 | overflow: hidden; | ||
145 | padding-top: 10px; | ||
146 | |||
147 | &:first-child { | ||
148 | padding-top: 30px; | ||
149 | } | ||
150 | |||
151 | my-video-miniature { | ||
152 | text-align: left; | ||
153 | } | ||
154 | |||
155 | .section-title { | ||
156 | font-size: 24px; | ||
157 | font-weight: $font-semibold; | ||
158 | margin-bottom: 30px; | ||
159 | display: flex; | ||
160 | justify-content: space-between; | ||
161 | |||
162 | a { | ||
163 | &:hover, &:focus:not(.focus-visible), &:active { | ||
164 | text-decoration: none; | ||
165 | outline: none; | ||
166 | } | ||
167 | |||
168 | color: var(--mainForegroundColor); | ||
169 | } | ||
170 | } | ||
171 | |||
172 | &.channel { | ||
173 | .section-title { | ||
174 | a { | ||
175 | display: flex; | ||
176 | width: fit-content; | ||
177 | align-items: center; | ||
178 | |||
179 | img { | ||
180 | @include avatar(28px); | ||
181 | |||
182 | margin-right: 8px; | ||
183 | } | ||
184 | } | ||
185 | |||
186 | .followers { | ||
187 | color: $grey-foreground-color; | ||
188 | font-weight: normal; | ||
189 | font-size: 14px; | ||
190 | margin-left: 10px; | ||
191 | position: relative; | ||
192 | top: 2px; | ||
193 | } | ||
194 | } | ||
195 | } | ||
196 | |||
197 | @media screen and (max-width: $mobile-view) { | ||
198 | max-height: initial; | ||
199 | overflow: initial; | ||
200 | |||
201 | @include video-miniature-small-screen; | ||
202 | |||
203 | .section-title { | ||
204 | font-size: 17px; | ||
205 | } | ||
206 | } | ||
207 | } | ||
208 | |||
209 | @mixin adapt-margin-content-width { | ||
210 | width: $video-miniature-width * 6; | ||
211 | margin: auto !important; | ||
212 | |||
213 | @media screen and (max-width: 1800px) { | ||
214 | width: $video-miniature-width * 5; | ||
215 | } | ||
216 | |||
217 | @media screen and (max-width: 1800px - $video-miniature-width) { | ||
218 | width: $video-miniature-width * 4; | ||
219 | } | ||
220 | |||
221 | @media screen and (max-width: 1800px - (2* $video-miniature-width)) { | ||
222 | width: $video-miniature-width * 3; | ||
223 | } | ||
224 | |||
225 | @media screen and (max-width: 1800px - (3* $video-miniature-width)) { | ||
226 | width: $video-miniature-width * 2; | ||
227 | } | ||
228 | |||
229 | @media screen and (max-width: 500px) { | ||
230 | width: auto; | ||
231 | margin: 0 !important; | ||
232 | |||
233 | .videos { | ||
234 | @include video-miniature-small-screen; | ||
235 | } | ||
236 | } | ||
237 | } | ||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 262a8136f..228a6116e 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -331,7 +331,12 @@ | |||
331 | } | 331 | } |
332 | 332 | ||
333 | @mixin peertube-checkbox ($border-width) { | 333 | @mixin peertube-checkbox ($border-width) { |
334 | display: none; | 334 | opacity: 0; |
335 | width: 0; | ||
336 | |||
337 | &:focus + span { | ||
338 | outline: auto; | ||
339 | } | ||
335 | 340 | ||
336 | & + span { | 341 | & + span { |
337 | position: relative; | 342 | position: relative; |